diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9771bbc..9b26acd 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,11 +8,12 @@ on: workflow_dispatch: jobs: + # The repo as people clone it: public classes plus SOQL Lib and DML Lib from dependencies/ salesforce-ci: - uses: beyond-the-cloud-dev/cicd-template/.github/workflows/salesforce-ci.yml@main + uses: beyond-the-cloud-dev/cicd-template/.github/workflows/salesforce-ci.yml@feat/deps-from-sfdx-project with: - # package/ needs SOQL Lib and DML Lib installed; force-app/ vendors both - source-dirs: 'force-app examples' + template-ref: feat/deps-from-sfdx-project + source-dirs: 'force-app' node-version: '22' sf-cli-version: 'latest' scratch-org-duration: 1 @@ -22,3 +23,36 @@ jobs: secrets: SFDX_AUTH_URL_DEVHUB: ${{ secrets.SFDX_AUTH_URL_DEVHUB }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + # The package as it gets built: `// global` declarations made global, SOQL Lib and + # DML Lib from the tags pinned in sfdx-project.json + package-check: + uses: beyond-the-cloud-dev/cicd-template/.github/workflows/salesforce-ci.yml@feat/deps-from-sfdx-project + with: + template-ref: feat/deps-from-sfdx-project + pre-deploy-command: 'node scripts/prepare-package.mjs' + deploy-dependencies: true + source-dirs: 'force-app' + pr-comment: false + node-version: '22' + sf-cli-version: 'latest' + scratch-org-duration: 1 + test-level: 'RunLocalTests' + secrets: + SFDX_AUTH_URL_DEVHUB: ${{ secrets.SFDX_AUTH_URL_DEVHUB }} + + dependencies: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npm run build:dependencies + - name: Check dependencies/ matches sfdx-project.json + run: | + if [ -n "$(git status --porcelain -- force-app/main/default/dependencies)" ]; then + git status --short -- force-app/main/default/dependencies + echo "::error::force-app/main/default/dependencies/ does not match the dependencies in sfdx-project.json. Run npm run build:dependencies and commit the result." + exit 1 + fi diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..7666237 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,51 @@ +name: Package version + +# Builds a new Trigger Lib package version from force-app/: declarations marked +# `// global` become global and dependencies/ is left out (SOQL Lib and DML Lib are +# package dependencies). Bump versionNumber in sfdx-project.json first. Promote the +# version by hand once it is tested. + +on: + workflow_dispatch: + +jobs: + create: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install Salesforce CLI + run: npm install @salesforce/cli --global + + - name: Authorize Dev Hub + env: + SFDX_AUTH_URL_DEVHUB: ${{ secrets.SFDX_AUTH_URL_DEVHUB }} + run: echo "$SFDX_AUTH_URL_DEVHUB" | sf org login sfdx-url --alias DevHub --set-default-dev-hub --sfdx-url-stdin + + - name: Prepare package source + run: node scripts/prepare-package.mjs + + - name: Create package version + run: | + sf package version create \ + --package "Trigger Lib" \ + --installation-key-bypass \ + --code-coverage \ + --wait 60 \ + --json > result.json || { cat result.json; exit 1; } + { + echo "## Trigger Lib package version" + echo "" + echo "- Subscriber package version: \`$(jq -r '.result.SubscriberPackageVersionId' result.json)\`" + echo "- Install: https://login.salesforce.com/packaging/installPackage.apexp?p0=$(jq -r '.result.SubscriberPackageVersionId' result.json)" + echo "" + echo "Add the new alias to \`packageAliases\` in \`sfdx-project.json\`:" + echo "" + echo '```diff' + git diff -U0 sfdx-project.json | grep '^[+-] ' || true + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6feb889 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +force-app/main/default/dependencies/ +README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d414c77..d728c1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -133,9 +133,28 @@ npm run docs:dev # Create scratch org for testing sf org create scratch -f config/project-scratch-def.json -a dev -sf project deploy start -o dev + +# Deploy Trigger Lib (SOQL Lib and DML Lib come along from force-app/main/default/dependencies/) +sf project deploy start -d force-app -o dev +``` + +### Project layout + +- `force-app/` is the source, with `public` classes, so a plain clone deploys to any org. +- `force-app/main/default/dependencies/` holds the SOQL Lib and DML Lib classes Trigger Lib needs, at the versions pinned in `sfdx-project.json`. It is generated: never edit it by hand. After bumping a dependency, run `npm run build:dependencies` and commit the result. CI fails when it does not match. +- `examples/` holds the example handlers used in the docs. It is not deployed by CI. + +### Package API + +The package is built from `force-app/` by `scripts/prepare-package.mjs`: it leaves out `dependencies/` and turns every declaration with a `// global` line right above it into `global`. Those markers are the package API. When you add a type or member that orgs installing the package must use, put `// global` above it: + +```apex +// global +public interface Writer extends Handler { ``` +The `package-check` CI job builds the package form on every PR and runs the tests on it, so a missing marker on a type used in a global signature fails there. New versions are created with the **Package version** workflow (Actions tab). + ## 📚 Resources - [Full Documentation](https://trigger.beyondthecloud.dev/) diff --git a/README.md b/README.md index 333d3c2..ef1682a 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,15 @@ public with sharing class AccountWelcomeTaskWriter implements AfterInsert.Writer } ``` +## Dependencies + +Trigger Lib is built on two other Apex Fluently libs, so they have to be in the org too: + +- [SOQL Lib](https://soql.beyondthecloud.dev) 6.12.0 +- [DML Lib](https://dml.beyondthecloud.dev) 4.0.0 + +Installing the packages? Install SOQL Lib and DML Lib first. The [installation guide](https://trigger.beyondthecloud.dev/installation) has the links in the right order. The button below and a plain `git clone` deploy both together with Trigger Lib, from `force-app/main/default/dependencies/`. + ## Deploy to Salesforce diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..45a2957 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,5 @@ +# Coverage counts Trigger Lib only. dependencies/ is a generated copy of SOQL Lib +# and DML Lib, examples/ is docs code. +ignore: + - 'force-app/main/default/dependencies/**' + - 'examples/**' diff --git a/force-app/main/default/classes/AfterDelete.cls b/force-app/main/default/classes/AfterDelete.cls index 74def77..004a18c 100644 --- a/force-app/main/default/classes/AfterDelete.cls +++ b/force-app/main/default/classes/AfterDelete.cls @@ -1,11 +1,14 @@ +// global public class AfterDelete { // Roles - pick one + // global public interface Writer extends Handler { Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record); void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork); } + // global public interface Dispatcher extends Handler { Boolean dispatchOnAfterDeleteWhen(TriggerTypes.DeleteRecord record); void dispatchOnAfterDelete(TriggerTypes.DeleteRecords records); @@ -13,36 +16,44 @@ public class AfterDelete { // Add-ons - pick as many as you need + // global public interface PriorParentQuery { Map queryPriorParentsOnAfterDelete(); } + // global public interface RelatedQuery { Map queryRelatedOnAfterDelete(); } + // global public interface RecordsProvider { List query(TriggerTypes.DeleteRecords records); String keyOf(SObject row); } + // global public interface OwnUnitOfWork { DML.Committable ownUnitOfWorkOnAfterDelete(); } + // global public interface Bypassable { Boolean bypassOnAfterDeleteWhen(); } + // global public interface Finalizer { void finalizeOnAfterDelete(TriggerTypes.DeleteRecords records); } + // global public interface ContinueOnError { } // Marker + // global public interface Handler { } diff --git a/force-app/main/default/classes/AfterInsert.cls b/force-app/main/default/classes/AfterInsert.cls index b44316c..65b6c64 100644 --- a/force-app/main/default/classes/AfterInsert.cls +++ b/force-app/main/default/classes/AfterInsert.cls @@ -1,11 +1,14 @@ +// global public class AfterInsert { // Roles - pick one + // global public interface Writer extends Handler { Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record); void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork); } + // global public interface Dispatcher extends Handler { Boolean dispatchOnAfterInsertWhen(TriggerTypes.InsertRecord record); void dispatchOnAfterInsert(TriggerTypes.InsertRecords records); @@ -13,36 +16,44 @@ public class AfterInsert { // Add-ons - pick as many as you need + // global public interface ParentQuery { Map queryParentsOnAfterInsert(); } + // global public interface RelatedQuery { Map queryRelatedOnAfterInsert(); } + // global public interface RecordsProvider { List query(TriggerTypes.InsertRecords records); String keyOf(SObject row); } + // global public interface OwnUnitOfWork { DML.Committable ownUnitOfWorkOnAfterInsert(); } + // global public interface Bypassable { Boolean bypassOnAfterInsertWhen(); } + // global public interface Finalizer { void finalizeOnAfterInsert(TriggerTypes.InsertRecords records); } + // global public interface ContinueOnError { } // Marker + // global public interface Handler { } diff --git a/force-app/main/default/classes/AfterUndelete.cls b/force-app/main/default/classes/AfterUndelete.cls index d91f693..1164471 100644 --- a/force-app/main/default/classes/AfterUndelete.cls +++ b/force-app/main/default/classes/AfterUndelete.cls @@ -1,11 +1,14 @@ +// global public class AfterUndelete { // Roles - pick one + // global public interface Writer extends Handler { Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record); void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork); } + // global public interface Dispatcher extends Handler { Boolean dispatchOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record); void dispatchOnAfterUndelete(TriggerTypes.UndeleteRecords records); @@ -13,36 +16,44 @@ public class AfterUndelete { // Add-ons - pick as many as you need + // global public interface ParentQuery { Map queryParentsOnAfterUndelete(); } + // global public interface RelatedQuery { Map queryRelatedOnAfterUndelete(); } + // global public interface RecordsProvider { List query(TriggerTypes.UndeleteRecords records); String keyOf(SObject row); } + // global public interface OwnUnitOfWork { DML.Committable ownUnitOfWorkOnAfterUndelete(); } + // global public interface Bypassable { Boolean bypassOnAfterUndeleteWhen(); } + // global public interface Finalizer { void finalizeOnAfterUndelete(TriggerTypes.UndeleteRecords records); } + // global public interface ContinueOnError { } // Marker + // global public interface Handler { } diff --git a/force-app/main/default/classes/AfterUpdate.cls b/force-app/main/default/classes/AfterUpdate.cls index 66616fe..f186466 100644 --- a/force-app/main/default/classes/AfterUpdate.cls +++ b/force-app/main/default/classes/AfterUpdate.cls @@ -1,11 +1,14 @@ +// global public class AfterUpdate { // Roles - pick one + // global public interface Writer extends Handler { Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record); void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork); } + // global public interface Dispatcher extends Handler { Boolean dispatchOnAfterUpdateWhen(TriggerTypes.UpdateRecord record); void dispatchOnAfterUpdate(TriggerTypes.UpdateRecords records); @@ -13,44 +16,54 @@ public class AfterUpdate { // Add-ons - pick as many as you need + // global public interface ParentQuery { Map queryParentsOnAfterUpdate(); } + // global public interface PriorParentQuery { Map queryPriorParentsOnAfterUpdate(); } + // global public interface RelatedQuery { Map queryRelatedOnAfterUpdate(); } + // global public interface RecordsProvider { List query(TriggerTypes.UpdateRecords records); String keyOf(SObject row); } + // global public interface OwnUnitOfWork { DML.Committable ownUnitOfWorkOnAfterUpdate(); } + // global public interface Bypassable { Boolean bypassOnAfterUpdateWhen(); } + // global public interface RecursionGuard { Integer maxRunsPerRecordOnAfterUpdate(); } + // global public interface Finalizer { void finalizeOnAfterUpdate(TriggerTypes.UpdateRecords records); } + // global public interface ContinueOnError { } // Marker + // global public interface Handler { } diff --git a/force-app/main/default/classes/BeforeDelete.cls b/force-app/main/default/classes/BeforeDelete.cls index 2578a5b..bd93de5 100644 --- a/force-app/main/default/classes/BeforeDelete.cls +++ b/force-app/main/default/classes/BeforeDelete.cls @@ -1,11 +1,14 @@ +// global public class BeforeDelete { // Roles - pick one + // global public interface Validator extends Handler { Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record); void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record); } + // global public interface Writer extends Handler { Boolean writeOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record); void writeOnBeforeDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork); @@ -13,36 +16,44 @@ public class BeforeDelete { // Add-ons - pick as many as you need + // global public interface PriorParentQuery { Map queryPriorParentsOnBeforeDelete(); } + // global public interface RelatedQuery { Map queryRelatedOnBeforeDelete(); } + // global public interface RecordsProvider { List query(TriggerTypes.DeleteRecords records); String keyOf(SObject row); } + // global public interface OwnUnitOfWork { DML.Committable ownUnitOfWorkOnBeforeDelete(); } + // global public interface Bypassable { Boolean bypassOnBeforeDeleteWhen(); } + // global public interface Finalizer { void finalizeOnBeforeDelete(TriggerTypes.DeleteRecords records); } + // global public interface ContinueOnError { } // Marker + // global public interface Handler { } diff --git a/force-app/main/default/classes/BeforeInsert.cls b/force-app/main/default/classes/BeforeInsert.cls index 5b7617d..0d0bffe 100644 --- a/force-app/main/default/classes/BeforeInsert.cls +++ b/force-app/main/default/classes/BeforeInsert.cls @@ -1,11 +1,14 @@ +// global public class BeforeInsert { // Roles - pick one + // global public interface Populator extends Handler { Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record); void populateOnBeforeInsert(TriggerTypes.InsertRecord record); } + // global public interface Validator extends Handler { Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record); void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record); @@ -13,32 +16,39 @@ public class BeforeInsert { // Add-ons - pick as many as you need + // global public interface ParentQuery { Map queryParentsOnBeforeInsert(); } + // global public interface RelatedQuery { Map queryRelatedOnBeforeInsert(); } + // global public interface RecordsProvider { List query(TriggerTypes.InsertRecords records); String keyOf(SObject row); } + // global public interface Bypassable { Boolean bypassOnBeforeInsertWhen(); } + // global public interface Finalizer { void finalizeOnBeforeInsert(TriggerTypes.InsertRecords records); } + // global public interface ContinueOnError { } // Marker + // global public interface Handler { } diff --git a/force-app/main/default/classes/BeforeUpdate.cls b/force-app/main/default/classes/BeforeUpdate.cls index fb09e11..d36f5a2 100644 --- a/force-app/main/default/classes/BeforeUpdate.cls +++ b/force-app/main/default/classes/BeforeUpdate.cls @@ -1,11 +1,14 @@ +// global public class BeforeUpdate { // Roles - pick one + // global public interface Populator extends Handler { Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record); void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record); } + // global public interface Validator extends Handler { Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record); void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record); @@ -13,40 +16,49 @@ public class BeforeUpdate { // Add-ons - pick as many as you need + // global public interface ParentQuery { Map queryParentsOnBeforeUpdate(); } + // global public interface PriorParentQuery { Map queryPriorParentsOnBeforeUpdate(); } + // global public interface RelatedQuery { Map queryRelatedOnBeforeUpdate(); } + // global public interface RecordsProvider { List query(TriggerTypes.UpdateRecords records); String keyOf(SObject row); } + // global public interface Bypassable { Boolean bypassOnBeforeUpdateWhen(); } + // global public interface RecursionGuard { Integer maxRunsPerRecordOnBeforeUpdate(); } + // global public interface Finalizer { void finalizeOnBeforeUpdate(TriggerTypes.UpdateRecords records); } + // global public interface ContinueOnError { } // Marker + // global public interface Handler { } diff --git a/force-app/main/default/classes/TriggerOrchestrator.cls b/force-app/main/default/classes/TriggerOrchestrator.cls index a38e59e..57fd0f4 100644 --- a/force-app/main/default/classes/TriggerOrchestrator.cls +++ b/force-app/main/default/classes/TriggerOrchestrator.cls @@ -17,40 +17,50 @@ @SuppressWarnings( 'PMD.ExcessiveClassLength,PMD.NcssTypeCount,PMD.NcssMethodCount,PMD.CyclomaticComplexity,PMD.StdCyclomaticComplexity,PMD.CognitiveComplexity,PMD.AvoidNonRestrictiveQueries,PMD.FieldDeclarationsShouldBeAtStart' ) +// global public with sharing class TriggerOrchestrator { + // global public interface BeforeInsert { List beforeInsertHandlers(); } + // global public interface AfterInsert { List afterInsertHandlers(); } + // global public interface BeforeUpdate { List beforeUpdateHandlers(); } + // global public interface AfterUpdate { List afterUpdateHandlers(); } + // global public interface BeforeDelete { List beforeDeleteHandlers(); } + // global public interface AfterDelete { List afterDeleteHandlers(); } + // global public interface AfterUndelete { List afterUndeleteHandlers(); } + // global public interface Logger { void log(TriggerOrchestrator.Error error); void flush(); } + // global public interface Error { String getHandlerName(); SObjectType getSObjectType(); @@ -59,14 +69,17 @@ public with sharing class TriggerOrchestrator { Set getRecordIds(); } + // global public static void run(Object orchestrator) { new TriggerOrchestrator(orchestrator).run(); } + // global public static Bypassable bypass() { return TriggerOrchestrator.triggerBypass; } + // global public interface Bypassable { Bypassable sObject(SObjectType sObjectType); Bypassable orchestrator(System.Type orchestrator); @@ -75,16 +88,19 @@ public with sharing class TriggerOrchestrator { void clear(); } + // global @TestVisible private static Mockable mock() { return TriggerOrchestrator.triggerMock; } + // global @TestVisible private static void runTestFor(Object handlerOrOrchestrator) { TriggerOrchestrator.triggerMock.nextContextFor(handlerOrOrchestrator).run(handlerOrOrchestrator); } + // global @TestVisible private interface Mockable { BeforeInsertMockable beforeInsertFor(System.Type handlerOrOrchestratorType); @@ -96,6 +112,7 @@ public with sharing class TriggerOrchestrator { AfterUndeleteMockable afterUndeleteFor(System.Type handlerOrOrchestratorType); } + // global public interface BeforeInsertMockable { BeforeInsertMockable with(SObject row); BeforeInsertMockable with(List rows); @@ -103,6 +120,7 @@ public with sharing class TriggerOrchestrator { BeforeInsertMockable withParent(SObjectField lookupField, List parents); } + // global public interface AfterInsertMockable { AfterInsertMockable with(SObject row); AfterInsertMockable with(List rows); @@ -110,6 +128,7 @@ public with sharing class TriggerOrchestrator { AfterInsertMockable withParent(SObjectField lookupField, List parents); } + // global public interface BeforeUpdateMockable { BeforeUpdateMockable with(SObject newRow, SObject oldRow); BeforeUpdateMockable with(List newRows, List oldRows); @@ -117,6 +136,7 @@ public with sharing class TriggerOrchestrator { BeforeUpdateMockable withParent(SObjectField lookupField, List parents); } + // global public interface AfterUpdateMockable { AfterUpdateMockable with(SObject newRow, SObject oldRow); AfterUpdateMockable with(List newRows, List oldRows); @@ -124,6 +144,7 @@ public with sharing class TriggerOrchestrator { AfterUpdateMockable withParent(SObjectField lookupField, List parents); } + // global public interface BeforeDeleteMockable { BeforeDeleteMockable with(SObject row); BeforeDeleteMockable with(List rows); @@ -131,6 +152,7 @@ public with sharing class TriggerOrchestrator { BeforeDeleteMockable withParent(SObjectField lookupField, List parents); } + // global public interface AfterDeleteMockable { AfterDeleteMockable with(SObject row); AfterDeleteMockable with(List rows); @@ -138,6 +160,7 @@ public with sharing class TriggerOrchestrator { AfterDeleteMockable withParent(SObjectField lookupField, List parents); } + // global public interface AfterUndeleteMockable { AfterUndeleteMockable with(SObject row); AfterUndeleteMockable with(List rows); diff --git a/force-app/main/default/classes/TriggerTypes.cls b/force-app/main/default/classes/TriggerTypes.cls index 058e53b..806f3a3 100644 --- a/force-app/main/default/classes/TriggerTypes.cls +++ b/force-app/main/default/classes/TriggerTypes.cls @@ -16,13 +16,16 @@ @SuppressWarnings( 'PMD.ExcessivePublicCount,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.ExcessiveParameterList,PMD.PropertyNamingConventions,PMD.FieldNamingConventions,PMD.FieldDeclarationsShouldBeAtStart' ) +// global public with sharing class TriggerTypes { + // global public static ParentFields ParentFields { get { return new TriggerParentFields(); } } + // global public interface InsertRecord { Boolean isRecordType(String recordTypeDeveloperName); Boolean isNotRecordType(String recordTypeDeveloperName); @@ -89,6 +92,7 @@ public with sharing class TriggerTypes { void put(SObjectField field, Object value); } + // global public interface RejectableInsertRecord { Boolean isRecordType(String recordTypeDeveloperName); Boolean isNotRecordType(String recordTypeDeveloperName); @@ -156,6 +160,7 @@ public with sharing class TriggerTypes { void addError(SObjectField field, String error); } + // global public interface UpdateRecord { Boolean isRecordType(String recordTypeDeveloperName); Boolean isNotRecordType(String recordTypeDeveloperName); @@ -243,6 +248,7 @@ public with sharing class TriggerTypes { void put(SObjectField field, Object value); } + // global public interface RejectableUpdateRecord { Boolean isRecordType(String recordTypeDeveloperName); Boolean isNotRecordType(String recordTypeDeveloperName); @@ -331,6 +337,7 @@ public with sharing class TriggerTypes { void addError(SObjectField field, String error); } + // global public interface DeleteRecord { Boolean isRecordType(String recordTypeDeveloperName); Boolean isNotRecordType(String recordTypeDeveloperName); @@ -395,6 +402,7 @@ public with sharing class TriggerTypes { TriggerTypes.RelatedRecords getRelated(String providerName); } + // global public interface RejectableDeleteRecord { Boolean isRecordType(String recordTypeDeveloperName); Boolean isNotRecordType(String recordTypeDeveloperName); @@ -462,6 +470,7 @@ public with sharing class TriggerTypes { void addError(SObjectField field, String error); } + // global public interface UndeleteRecord { Boolean isRecordType(String recordTypeDeveloperName); Boolean isNotRecordType(String recordTypeDeveloperName); @@ -526,6 +535,7 @@ public with sharing class TriggerTypes { TriggerTypes.RelatedRecords getRelated(String providerName); } + // global public interface InsertRecords { Set getIds(); @@ -540,6 +550,7 @@ public with sharing class TriggerTypes { Integer size(); } + // global public interface UpdateRecords { Set getIds(); @@ -558,6 +569,7 @@ public with sharing class TriggerTypes { Integer size(); } + // global public interface DeleteRecords { Set getIds(); @@ -572,6 +584,7 @@ public with sharing class TriggerTypes { Integer size(); } + // global public interface UndeleteRecords { Set getIds(); @@ -586,6 +599,7 @@ public with sharing class TriggerTypes { Integer size(); } + // global public interface UnitOfWork { UnitOfWork toInsert(SObject record); UnitOfWork toInsert(DML.Record record); @@ -600,6 +614,7 @@ public with sharing class TriggerTypes { UnitOfWork toPublish(SObject event); } + // global public interface ParentFields { ParentFields with(SObjectField field); ParentFields with(SObjectField field1, SObjectField field2); @@ -619,6 +634,7 @@ public with sharing class TriggerTypes { List getFields(); } + // global public interface RelatedRecords { SObject getFirstWhereKeyEquals(Object key); @@ -1505,13 +1521,16 @@ public with sharing class TriggerTypes { } } + // global public class RandomIdGenerator { private final String RANDOM_STRING_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; + // global public Id get(SObjectType objectType) { return this.get(objectType.getDescribe(SObjectDescribeOptions.DEFERRED).getKeyPrefix()); } + // global public Id get(String prefix) { String randomPart = ''; @@ -1524,6 +1543,7 @@ public with sharing class TriggerTypes { } } + // global public class TriggerLibException extends Exception { } } diff --git a/force-app/main/default/dependencies/dml-lib/DML.cls-meta.xml b/force-app/main/default/dependencies/dml-lib/DML.cls-meta.xml index 7d3a8b2..cad713d 100644 --- a/force-app/main/default/dependencies/dml-lib/DML.cls-meta.xml +++ b/force-app/main/default/dependencies/dml-lib/DML.cls-meta.xml @@ -1,5 +1,5 @@ - + - 67.0 + 66.0 Active diff --git a/force-app/main/default/dependencies/dml-lib/DML_Test.cls b/force-app/main/default/dependencies/dml-lib/DML_Test.cls index 3ce699d..2b6d3ca 100644 --- a/force-app/main/default/dependencies/dml-lib/DML_Test.cls +++ b/force-app/main/default/dependencies/dml-lib/DML_Test.cls @@ -12,7 +12,7 @@ * - NcssTypeCount: It is a library and we tried to put everything into ONE class **/ @SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.ApexUnitTestClassShouldHaveRunAs,PMD.NcssMethodCount,PMD.NcssTypeCount') -@IsTest(IsParallel=true testFor='ApexClass:DML') +@IsTest private class DML_Test { @IsTest diff --git a/force-app/main/default/dependencies/dml-lib/DML_Test.cls-meta.xml b/force-app/main/default/dependencies/dml-lib/DML_Test.cls-meta.xml index 7d3a8b2..cad713d 100644 --- a/force-app/main/default/dependencies/dml-lib/DML_Test.cls-meta.xml +++ b/force-app/main/default/dependencies/dml-lib/DML_Test.cls-meta.xml @@ -1,5 +1,5 @@ - + - 67.0 + 66.0 Active diff --git a/force-app/main/default/dependencies/soql-lib/SOQL.cls-meta.xml b/force-app/main/default/dependencies/soql-lib/SOQL.cls-meta.xml index 7d3a8b2..356da04 100644 --- a/force-app/main/default/dependencies/soql-lib/SOQL.cls-meta.xml +++ b/force-app/main/default/dependencies/soql-lib/SOQL.cls-meta.xml @@ -1,5 +1,5 @@ - 67.0 + 66.0 Active diff --git a/force-app/main/default/dependencies/soql-lib/SOQL_Test.cls b/force-app/main/default/dependencies/soql-lib/SOQL_Test.cls index 9b10db3..623281c 100644 --- a/force-app/main/default/dependencies/soql-lib/SOQL_Test.cls +++ b/force-app/main/default/dependencies/soql-lib/SOQL_Test.cls @@ -11,7 +11,7 @@ * - NcssTypeCount: It is a library and we tried to put everything into ONE test class **/ @SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount') -@IsTest(IsParallel=true testFor='ApexClass:SOQL') +@IsTest(IsParallel=true) private class SOQL_Test { @IsTest static void ofSObjectType() { diff --git a/force-app/main/default/dependencies/soql-lib/SOQL_Test.cls-meta.xml b/force-app/main/default/dependencies/soql-lib/SOQL_Test.cls-meta.xml index 7d3a8b2..356da04 100644 --- a/force-app/main/default/dependencies/soql-lib/SOQL_Test.cls-meta.xml +++ b/force-app/main/default/dependencies/soql-lib/SOQL_Test.cls-meta.xml @@ -1,5 +1,5 @@ - 67.0 + 66.0 Active diff --git a/package.json b/package.json index 3b9995b..f926951 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "precommit": "lint-staged", "docs:dev": "vitepress dev website", "docs:build": "vitepress build website", - "docs:preview": "vitepress preview website" + "docs:preview": "vitepress preview website", + "build:dependencies": "node scripts/build-dependencies.mjs" }, "devDependencies": { "@lwc/eslint-plugin-lwc": "^3.3.0", diff --git a/package/main/default/classes/AfterDelete.cls b/package/main/default/classes/AfterDelete.cls deleted file mode 100644 index 5139757..0000000 --- a/package/main/default/classes/AfterDelete.cls +++ /dev/null @@ -1,279 +0,0 @@ -global class AfterDelete { - // Roles - pick one - - global interface Writer extends Handler { - Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record); - void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork); - } - - global interface Dispatcher extends Handler { - Boolean dispatchOnAfterDeleteWhen(TriggerTypes.DeleteRecord record); - void dispatchOnAfterDelete(TriggerTypes.DeleteRecords records); - } - - // Add-ons - pick as many as you need - - global interface PriorParentQuery { - Map queryPriorParentsOnAfterDelete(); - } - - global interface RelatedQuery { - Map queryRelatedOnAfterDelete(); - } - - global interface RecordsProvider { - List query(TriggerTypes.DeleteRecords records); - String keyOf(SObject row); - } - - global interface OwnUnitOfWork { - DML.Committable ownUnitOfWorkOnAfterDelete(); - } - - global interface Bypassable { - Boolean bypassOnAfterDeleteWhen(); - } - - global interface Finalizer { - void finalizeOnAfterDelete(TriggerTypes.DeleteRecords records); - } - - global interface ContinueOnError { - } - - // Marker - - global interface Handler { - } - - // Implementation - - public class Operation extends TriggerOrchestrator.Operation { - public override List buildExecutors(TriggerOrchestrator orchestrator) { - List executors = new List(); - - for (AfterDelete.Handler handler : ((TriggerOrchestrator.AfterDelete) orchestrator.orchestrator).afterDeleteHandlers()) { - if (handler instanceof AfterDelete.Writer) { - executors.add(new AfterDelete.WriterExecutor((AfterDelete.Writer) handler, orchestrator)); - } else if (handler instanceof AfterDelete.Dispatcher) { - executors.add(new AfterDelete.DispatcherExecutor((AfterDelete.Dispatcher) handler, orchestrator)); - } - } - - return executors; - } - } - - public class WriterExecutor extends TriggerOrchestrator.HandlerExecutor { - private AfterDelete.Writer writer; - private TriggerOrchestrator.UnitOfWork unitOfWork; - - public WriterExecutor(AfterDelete.Writer writer, TriggerOrchestrator orchestrator) { - super(writer, orchestrator); - - this.writer = writer; - this.parentDeclaration = new AfterDelete.ParentDeclaration(this.writer); - this.relatedLoader = new AfterDelete.RelatedLoader(this.writer); - this.unitOfWork = new AfterDelete.UnitOfWorkResolver(this.writer).resolve(this.orchestrator.sharedUnitOfWork); - this.handlerHooks = new AfterDelete.HandlerHooks(this.writer); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.write(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - this.commitOwnUnitOfWork(); - } catch (Exception e) { - this.handleException(e); - } - } - - private void write(TriggerTypes.TriggerRecord record) { - if (this.writer.writeOnAfterDeleteWhen(record)) { - this.qualifiedRecords.add(record); - this.writer.writeOnAfterDelete(record, this.unitOfWork); - } - } - - private void commitOwnUnitOfWork() { - if (this.qualifiedRecords.isEmpty() || this.unitOfWork === this.orchestrator.sharedUnitOfWork) { - return; - } - - this.unitOfWork.commitWork(); - } - } - - public class DispatcherExecutor extends TriggerOrchestrator.HandlerExecutor { - private AfterDelete.Dispatcher dispatcher; - - public DispatcherExecutor(AfterDelete.Dispatcher dispatcher, TriggerOrchestrator orchestrator) { - super(dispatcher, orchestrator); - - this.dispatcher = dispatcher; - this.parentDeclaration = new AfterDelete.ParentDeclaration(this.dispatcher); - this.relatedLoader = new AfterDelete.RelatedLoader(this.dispatcher); - this.handlerHooks = new AfterDelete.HandlerHooks(this.dispatcher); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.qualify(record); - } - - this.dispatch(); - this.handlerHooks.finalize(this.qualifiedRecords); - } catch (Exception e) { - this.handleException(e); - } - } - - private void qualify(TriggerTypes.TriggerRecord record) { - if (this.dispatcher.dispatchOnAfterDeleteWhen(record)) { - this.qualifiedRecords.add(record); - } - } - - private void dispatch() { - if (this.qualifiedRecords.isEmpty()) { - return; - } - - this.dispatcher.dispatchOnAfterDelete(new TriggerTypes.DeleteTriggerRecords(this.qualifiedRecords)); - } - } - - public class ParentDeclaration extends TriggerOrchestrator.ParentDeclaration { - private AfterDelete.Handler handler; - - public ParentDeclaration(AfterDelete.Handler handler) { - this.handler = handler; - } - - public override Map getOldParentFields() { - if (this.handler instanceof AfterDelete.PriorParentQuery) { - return ((AfterDelete.PriorParentQuery) this.handler).queryPriorParentsOnAfterDelete(); - } - - return new Map(); - } - } - - public class RelatedLoader extends TriggerOrchestrator.RelatedLoader { - private AfterDelete.Handler handler; - private Map providers; - - public RelatedLoader(AfterDelete.Handler handler) { - this.handler = handler; - } - - public override TriggerTypes.RelatedRecords queryRelated(String providerName) { - if (!(this.handler instanceof AfterDelete.RelatedQuery)) { - return null; - } - - if (this.providers == null) { - this.providers = ((AfterDelete.RelatedQuery) this.handler).queryRelatedOnAfterDelete() ?? new Map(); - } - - AfterDelete.RecordsProvider provider = this.providers.get(providerName); - - if (provider == null) { - return null; - } - - TriggerTypes.DeleteRecords deleteRecords = new TriggerTypes.DeleteTriggerRecords(this.records); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(provider.query(deleteRecords)); - - for (SObject row : related.getRecords()) { - related.groupUnderKey(provider.keyOf(row), row); - } - - return related; - } - } - - public class UnitOfWorkResolver extends TriggerOrchestrator.UnitOfWorkResolver { - private AfterDelete.Writer writer; - - public UnitOfWorkResolver(AfterDelete.Writer writer) { - this.writer = writer; - } - - public override TriggerOrchestrator.UnitOfWork resolve(TriggerOrchestrator.UnitOfWork sharedUnitOfWork) { - if (this.writer instanceof AfterDelete.OwnUnitOfWork) { - return new TriggerOrchestrator.UnitOfWork(((AfterDelete.OwnUnitOfWork) this.writer).ownUnitOfWorkOnAfterDelete()); - } - - if (this.writer instanceof AfterDelete.ContinueOnError) { - return new TriggerOrchestrator.UnitOfWork(); - } - - return sharedUnitOfWork; - } - } - - public class HandlerHooks extends TriggerOrchestrator.HandlerHooks { - private AfterDelete.Handler handler; - - public HandlerHooks(AfterDelete.Handler handler) { - this.handler = handler; - } - - public override Boolean isBypassedByAddOn() { - return (this.handler instanceof AfterDelete.Bypassable && ((AfterDelete.Bypassable) this.handler).bypassOnAfterDeleteWhen()); - } - - public override void finalize(List qualifiedRecords) { - if (qualifiedRecords.isEmpty() || !(this.handler instanceof AfterDelete.Finalizer)) { - return; - } - - ((AfterDelete.Finalizer) this.handler).finalizeOnAfterDelete(new TriggerTypes.DeleteTriggerRecords(qualifiedRecords)); - } - - public override Boolean continuesOnError() { - return this.handler instanceof AfterDelete.ContinueOnError; - } - } - - public class MockedContext extends TriggerOrchestrator.MockedContext implements TriggerOrchestrator.AfterDeleteMockable, TriggerOrchestrator.AfterDelete { - public MockedContext() { - super(System.TriggerOperation.AFTER_DELETE); - } - - public TriggerOrchestrator.AfterDeleteMockable with(SObject row) { - return this.with(new List{ row }); - } - - public TriggerOrchestrator.AfterDeleteMockable with(List rows) { - this.oldRecords.addAll(this.assignIds(rows)); - return this; - } - - public TriggerOrchestrator.AfterDeleteMockable withParent(SObjectField lookupField, SObject parent) { - return this.withParent(lookupField, new List{ parent }); - } - - public TriggerOrchestrator.AfterDeleteMockable withParent(SObjectField lookupField, List parents) { - this.addParents(lookupField, parents); - return this; - } - - public List afterDeleteHandlers() { - if (this.handlerOrOrchestrator instanceof TriggerOrchestrator.AfterDelete) { - return ((TriggerOrchestrator.AfterDelete) this.handlerOrOrchestrator).afterDeleteHandlers(); - } - - return new List{ (AfterDelete.Handler) this.handlerOrOrchestrator }; - } - } -} diff --git a/package/main/default/classes/AfterDelete.cls-meta.xml b/package/main/default/classes/AfterDelete.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/AfterDelete.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/AfterDeleteTest.cls b/package/main/default/classes/AfterDeleteTest.cls deleted file mode 100644 index 05e3160..0000000 --- a/package/main/default/classes/AfterDeleteTest.cls +++ /dev/null @@ -1,725 +0,0 @@ -/** - * @description Tests for the AfterDelete context of the TriggerOrchestrator class. - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class - * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexUnitTestClassShouldHaveRunAs: The lib does not branch on the running user, so running as one proves nothing - **/ -@SuppressWarnings('PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexUnitTestClassShouldHaveRunAs') -@IsTest(IsParallel=true testFor='ApexClass:AfterDelete, ApexClass:TriggerOrchestrator') -private class AfterDeleteTest { - @IsTest - static void writerWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(AccountWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The writer should register the account.'); - } - - @IsTest - static void dispatcherWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterDeleteFor(IdDispatcher.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The dispatcher should receive the qualified records.'); - } - - @IsTest - static void writerWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(AccountWriterWithBypassable.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithBypassable()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The handler should not run.'); - } - - @IsTest - static void dispatcherWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcherWithBypassable dispatcher = new IdDispatcherWithBypassable(); - - TriggerOrchestrator.mock().afterDeleteFor(IdDispatcherWithBypassable.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.isTrue(dispatcher.dispatchedIds.isEmpty(), 'The handler should not run.'); - } - - @IsTest - static void writer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(AccountWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The writer should register the account.'); - } - - @IsTest - static void writerWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - TriggerOrchestrator.mock().afterDeleteFor(AccountWriter.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The writer should not register an account.'); - } - - @IsTest - static void dispatcher() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterDeleteFor(IdDispatcher.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The dispatcher should receive the qualified records.'); - } - - @IsTest - static void dispatcherWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - BatchDispatcher dispatcher = new BatchDispatcher(); - - TriggerOrchestrator.mock().afterDeleteFor(BatchDispatcher.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(0, dispatcher.dispatchedBatches.size(), 'The dispatcher should not be called.'); - } - - @IsTest - static void dispatcherWithMultipleRecords() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact johnDoe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), FirstName = 'John', LastName = 'Doe'); - Contact janeDoe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), FirstName = 'Jane', LastName = 'Doe'); - - BatchDispatcher dispatcher = new BatchDispatcher(); - - TriggerOrchestrator.mock().afterDeleteFor(BatchDispatcher.class).with(new List{ johnDoe, janeDoe }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List>{ new Set{ johnDoe.Id, janeDoe.Id } }, dispatcher.dispatchedBatches, 'The dispatcher should receive all records in one call.'); - } - - @IsTest - static void dispatcherBeforeFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcherWithFinalizerSnapshot dispatcher = new IdDispatcherWithFinalizerSnapshot(); - - TriggerOrchestrator.mock().afterDeleteFor(IdDispatcherWithFinalizerSnapshot.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIdsWhenFinalized, 'The dispatcher should dispatch before it finalizes.'); - } - - @IsTest - static void writerWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - AccountWriterWithFinalizer writer = new AccountWriterWithFinalizer(); - - TriggerOrchestrator.mock().afterDeleteFor(AccountWriterWithFinalizer.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.areEqual(new Set{ doe.Id }, writer.finalizedIds, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void dispatcherWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - IdDispatcherWithFinalizer dispatcher = new IdDispatcherWithFinalizer(); - - TriggerOrchestrator.mock().afterDeleteFor(IdDispatcherWithFinalizer.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new Set{ doe.Id }, dispatcher.finalizedIds, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void writerWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - AccountWriterWithFinalizer writer = new AccountWriterWithFinalizer(); - - TriggerOrchestrator.mock().afterDeleteFor(AccountWriterWithFinalizer.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.isNull(writer.finalizedIds, 'The finalizer should not run.'); - } - - @IsTest - static void dispatcherWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - IdDispatcherWithFinalizer dispatcher = new IdDispatcherWithFinalizer(); - - TriggerOrchestrator.mock().afterDeleteFor(IdDispatcherWithFinalizer.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.isNull(dispatcher.finalizedIds, 'The finalizer should not run.'); - } - - @IsTest - static void writerWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(FailingWriter.class).with(doe); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingWriter()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The writer failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void dispatcherWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(FailingDispatcher.class).with(doe); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDispatcher()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The dispatcher failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void writerWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(FailingWriterWithContinueOnError.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new FailingWriterWithContinueOnError()); - - // Verify - Assert.areEqual('The writer failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void dispatcherWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(FailingDispatcherWithContinueOnError.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new FailingDispatcherWithContinueOnError()); - - // Verify - Assert.areEqual('The dispatcher failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void priorParentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().afterDeleteFor(PriorAccountNameWriter.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new PriorAccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The writer should copy the prior account name.'); - } - - @IsTest - static void relatedQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().afterDeleteFor(RelatedAccountNameWriter.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new RelatedAccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The writer should copy the related account name.'); - } - - @IsTest - static void writerWithSharedUnitOfWorkWhenNextHandlerRuns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - CommitCountingDispatcher dispatcher = new CommitCountingDispatcher(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterDelete.add(new AccountWriter()); - handlers.afterDelete.add(dispatcher); - - TriggerOrchestrator.mock().afterDeleteFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(0, dispatcher.committedAccounts, 'The shared unit of work should not commit before the next handler.'); - } - - @IsTest - static void writerWithOwnUnitOfWork() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('ownUnit').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(AccountWriterWithOwnUnitOfWork.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithOwnUnitOfWork()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('ownUnit').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The own unit of work should commit the account.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenNextHandlerRuns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - CommitCountingDispatcher dispatcher = new CommitCountingDispatcher(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterDelete.add(new AccountWriterWithContinueOnError()); - handlers.afterDelete.add(dispatcher); - - TriggerOrchestrator.mock().afterDeleteFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(1, dispatcher.committedAccounts, 'The automatic unit of work should commit before the next handler.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenWriteFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(FailingAccountWriterWithContinueOnError.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new FailingAccountWriterWithContinueOnError()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The automatic unit of work should not commit.'); - } - - @IsTest - static void mockWith() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterDeleteFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('AFTER_DELETE', doe.Description, 'The mocked context should run.'); - } - - @IsTest - static void mockWithParent() { - // Setup - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().afterDeleteFor(PriorAccountNameWriter.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new PriorAccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The mocked parent should be served.'); - } - - private class OperationStamper implements AfterDelete.Writer { - public Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getOldSObject().put(Contact.Description, 'AFTER_DELETE'); - } - } - - private class AccountWriterWithBypassable extends AccountWriter implements AfterDelete.Bypassable { - public Boolean bypassOnAfterDeleteWhen() { - return true; - } - } - - private class IdDispatcherWithBypassable extends IdDispatcher implements AfterDelete.Bypassable { - public Boolean bypassOnAfterDeleteWhen() { - return true; - } - } - - private class BatchDispatcher implements AfterDelete.Dispatcher { - public List> dispatchedBatches = new List>(); - - public Boolean dispatchOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterDelete(TriggerTypes.DeleteRecords records) { - this.dispatchedBatches.add(records.getIds()); - } - } - - private class IdDispatcherWithFinalizerSnapshot extends IdDispatcher implements AfterDelete.Finalizer { - public List dispatchedIdsWhenFinalized; - - public void finalizeOnAfterDelete(TriggerTypes.DeleteRecords records) { - this.dispatchedIdsWhenFinalized = this.dispatchedIds.clone(); - } - } - - private class AccountWriterWithFinalizer extends AccountWriter implements AfterDelete.Finalizer { - public Set finalizedIds; - - public void finalizeOnAfterDelete(TriggerTypes.DeleteRecords records) { - this.finalizedIds = records.getIds(); - } - } - - private class IdDispatcherWithFinalizer extends IdDispatcher implements AfterDelete.Finalizer { - public Set finalizedIds; - - public void finalizeOnAfterDelete(TriggerTypes.DeleteRecords records) { - this.finalizedIds = records.getIds(); - } - } - - private virtual class FailingWriter implements AfterDelete.Writer { - public Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - throw new TestException('The writer failed.'); - } - } - - private class FailingWriterWithContinueOnError extends FailingWriter implements AfterDelete.ContinueOnError { - } - - private virtual class FailingDispatcher implements AfterDelete.Dispatcher { - public Boolean dispatchOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterDelete(TriggerTypes.DeleteRecords records) { - throw new TestException('The dispatcher failed.'); - } - } - - private class FailingDispatcherWithContinueOnError extends FailingDispatcher implements AfterDelete.ContinueOnError { - } - - private class PriorAccountNameWriter implements AfterDelete.Writer, AfterDelete.PriorParentQuery { - public Map queryPriorParentsOnAfterDelete() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Task(Subject = (String) record.getOldParent(Contact.AccountId)?.get(Account.Name))); - } - } - - private class RelatedAccountProvider implements AfterDelete.RecordsProvider { - public List query(TriggerTypes.DeleteRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class RelatedAccountNameWriter implements AfterDelete.Writer, AfterDelete.RelatedQuery { - public Map queryRelatedOnAfterDelete() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert( - new Task(Subject = (String) record.getRelated('accounts').getFirstWhereKeyEquals(record.getOldSObject().get(Contact.AccountId))?.get(Account.Name)) - ); - } - } - - private class CommitCountingDispatcher implements AfterDelete.Dispatcher { - public Integer committedAccounts; - - public Boolean dispatchOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterDelete(TriggerTypes.DeleteRecords records) { - this.committedAccounts = DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(); - } - } - - private virtual class AccountWriterWithOwnUnitOfWork extends AccountWriter implements AfterDelete.OwnUnitOfWork { - public DML.Committable ownUnitOfWorkOnAfterDelete() { - return new DML().identifier('ownUnit'); - } - } - - private class AccountWriterWithContinueOnError extends AccountWriter implements AfterDelete.ContinueOnError { - } - - private class FailingAccountWriterWithContinueOnError implements AfterDelete.Writer, AfterDelete.ContinueOnError { - public Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getOldSObject().get(Contact.LastName))); - throw new TestException('The writer failed.'); - } - } - - private class TestOrchestrator implements TriggerOrchestrator.AfterDelete { - public List afterDelete = new List(); - - public List afterDeleteHandlers() { - return this.afterDelete; - } - } - - private virtual class AccountWriter implements AfterDelete.Writer { - public Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getOldSObject().get(Contact.LastName))); - } - } - - private virtual class IdDispatcher implements AfterDelete.Dispatcher { - public List dispatchedIds = new List(); - - public Boolean dispatchOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterDelete(TriggerTypes.DeleteRecords records) { - this.dispatchedIds.addAll(records.getIds()); - } - } - - public class TestLogger implements TriggerOrchestrator.Logger { - public List errors = new List(); - public Integer flushCalls = 0; - - public void log(TriggerOrchestrator.Error error) { - this.errors.add(error); - } - - public void flush() { - this.flushCalls++; - } - } - - private class TestException extends Exception { - } -} diff --git a/package/main/default/classes/AfterDeleteTest.cls-meta.xml b/package/main/default/classes/AfterDeleteTest.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/AfterDeleteTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/AfterInsert.cls b/package/main/default/classes/AfterInsert.cls deleted file mode 100644 index 0be87e3..0000000 --- a/package/main/default/classes/AfterInsert.cls +++ /dev/null @@ -1,279 +0,0 @@ -global class AfterInsert { - // Roles - pick one - - global interface Writer extends Handler { - Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record); - void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork); - } - - global interface Dispatcher extends Handler { - Boolean dispatchOnAfterInsertWhen(TriggerTypes.InsertRecord record); - void dispatchOnAfterInsert(TriggerTypes.InsertRecords records); - } - - // Add-ons - pick as many as you need - - global interface ParentQuery { - Map queryParentsOnAfterInsert(); - } - - global interface RelatedQuery { - Map queryRelatedOnAfterInsert(); - } - - global interface RecordsProvider { - List query(TriggerTypes.InsertRecords records); - String keyOf(SObject row); - } - - global interface OwnUnitOfWork { - DML.Committable ownUnitOfWorkOnAfterInsert(); - } - - global interface Bypassable { - Boolean bypassOnAfterInsertWhen(); - } - - global interface Finalizer { - void finalizeOnAfterInsert(TriggerTypes.InsertRecords records); - } - - global interface ContinueOnError { - } - - // Marker - - global interface Handler { - } - - // Implementation - - public class Operation extends TriggerOrchestrator.Operation { - public override List buildExecutors(TriggerOrchestrator orchestrator) { - List executors = new List(); - - for (AfterInsert.Handler handler : ((TriggerOrchestrator.AfterInsert) orchestrator.orchestrator).afterInsertHandlers()) { - if (handler instanceof AfterInsert.Writer) { - executors.add(new AfterInsert.WriterExecutor((AfterInsert.Writer) handler, orchestrator)); - } else if (handler instanceof AfterInsert.Dispatcher) { - executors.add(new AfterInsert.DispatcherExecutor((AfterInsert.Dispatcher) handler, orchestrator)); - } - } - - return executors; - } - } - - public class WriterExecutor extends TriggerOrchestrator.HandlerExecutor { - private AfterInsert.Writer writer; - private TriggerOrchestrator.UnitOfWork unitOfWork; - - public WriterExecutor(AfterInsert.Writer writer, TriggerOrchestrator orchestrator) { - super(writer, orchestrator); - - this.writer = writer; - this.parentDeclaration = new AfterInsert.ParentDeclaration(this.writer); - this.relatedLoader = new AfterInsert.RelatedLoader(this.writer); - this.unitOfWork = new AfterInsert.UnitOfWorkResolver(this.writer).resolve(this.orchestrator.sharedUnitOfWork); - this.handlerHooks = new AfterInsert.HandlerHooks(this.writer); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.write(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - this.commitOwnUnitOfWork(); - } catch (Exception e) { - this.handleException(e); - } - } - - private void write(TriggerTypes.TriggerRecord record) { - if (this.writer.writeOnAfterInsertWhen(record)) { - this.qualifiedRecords.add(record); - this.writer.writeOnAfterInsert(record, this.unitOfWork); - } - } - - private void commitOwnUnitOfWork() { - if (this.qualifiedRecords.isEmpty() || this.unitOfWork === this.orchestrator.sharedUnitOfWork) { - return; - } - - this.unitOfWork.commitWork(); - } - } - - public class DispatcherExecutor extends TriggerOrchestrator.HandlerExecutor { - private AfterInsert.Dispatcher dispatcher; - - public DispatcherExecutor(AfterInsert.Dispatcher dispatcher, TriggerOrchestrator orchestrator) { - super(dispatcher, orchestrator); - - this.dispatcher = dispatcher; - this.parentDeclaration = new AfterInsert.ParentDeclaration(this.dispatcher); - this.relatedLoader = new AfterInsert.RelatedLoader(this.dispatcher); - this.handlerHooks = new AfterInsert.HandlerHooks(this.dispatcher); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.qualify(record); - } - - this.dispatch(); - this.handlerHooks.finalize(this.qualifiedRecords); - } catch (Exception e) { - this.handleException(e); - } - } - - private void qualify(TriggerTypes.TriggerRecord record) { - if (this.dispatcher.dispatchOnAfterInsertWhen(record)) { - this.qualifiedRecords.add(record); - } - } - - private void dispatch() { - if (this.qualifiedRecords.isEmpty()) { - return; - } - - this.dispatcher.dispatchOnAfterInsert(new TriggerTypes.InsertTriggerRecords(this.qualifiedRecords)); - } - } - - public class ParentDeclaration extends TriggerOrchestrator.ParentDeclaration { - private AfterInsert.Handler handler; - - public ParentDeclaration(AfterInsert.Handler handler) { - this.handler = handler; - } - - public override Map getNewParentFields() { - if (this.handler instanceof AfterInsert.ParentQuery) { - return ((AfterInsert.ParentQuery) this.handler).queryParentsOnAfterInsert(); - } - - return new Map(); - } - } - - public class RelatedLoader extends TriggerOrchestrator.RelatedLoader { - private AfterInsert.Handler handler; - private Map providers; - - public RelatedLoader(AfterInsert.Handler handler) { - this.handler = handler; - } - - public override TriggerTypes.RelatedRecords queryRelated(String providerName) { - if (!(this.handler instanceof AfterInsert.RelatedQuery)) { - return null; - } - - if (this.providers == null) { - this.providers = ((AfterInsert.RelatedQuery) this.handler).queryRelatedOnAfterInsert() ?? new Map(); - } - - AfterInsert.RecordsProvider provider = this.providers.get(providerName); - - if (provider == null) { - return null; - } - - TriggerTypes.InsertRecords insertRecords = new TriggerTypes.InsertTriggerRecords(this.records); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(provider.query(insertRecords)); - - for (SObject row : related.getRecords()) { - related.groupUnderKey(provider.keyOf(row), row); - } - - return related; - } - } - - public class UnitOfWorkResolver extends TriggerOrchestrator.UnitOfWorkResolver { - private AfterInsert.Writer writer; - - public UnitOfWorkResolver(AfterInsert.Writer writer) { - this.writer = writer; - } - - public override TriggerOrchestrator.UnitOfWork resolve(TriggerOrchestrator.UnitOfWork sharedUnitOfWork) { - if (this.writer instanceof AfterInsert.OwnUnitOfWork) { - return new TriggerOrchestrator.UnitOfWork(((AfterInsert.OwnUnitOfWork) this.writer).ownUnitOfWorkOnAfterInsert()); - } - - if (this.writer instanceof AfterInsert.ContinueOnError) { - return new TriggerOrchestrator.UnitOfWork(); - } - - return sharedUnitOfWork; - } - } - - public class HandlerHooks extends TriggerOrchestrator.HandlerHooks { - private AfterInsert.Handler handler; - - public HandlerHooks(AfterInsert.Handler handler) { - this.handler = handler; - } - - public override Boolean isBypassedByAddOn() { - return (this.handler instanceof AfterInsert.Bypassable && ((AfterInsert.Bypassable) this.handler).bypassOnAfterInsertWhen()); - } - - public override void finalize(List qualifiedRecords) { - if (qualifiedRecords.isEmpty() || !(this.handler instanceof AfterInsert.Finalizer)) { - return; - } - - ((AfterInsert.Finalizer) this.handler).finalizeOnAfterInsert(new TriggerTypes.InsertTriggerRecords(qualifiedRecords)); - } - - public override Boolean continuesOnError() { - return this.handler instanceof AfterInsert.ContinueOnError; - } - } - - public class MockedContext extends TriggerOrchestrator.MockedContext implements TriggerOrchestrator.AfterInsertMockable, TriggerOrchestrator.AfterInsert { - public MockedContext() { - super(System.TriggerOperation.AFTER_INSERT); - } - - public TriggerOrchestrator.AfterInsertMockable with(SObject row) { - return this.with(new List{ row }); - } - - public TriggerOrchestrator.AfterInsertMockable with(List rows) { - this.newRecords.addAll(this.assignIds(rows)); - return this; - } - - public TriggerOrchestrator.AfterInsertMockable withParent(SObjectField lookupField, SObject parent) { - return this.withParent(lookupField, new List{ parent }); - } - - public TriggerOrchestrator.AfterInsertMockable withParent(SObjectField lookupField, List parents) { - this.addParents(lookupField, parents); - return this; - } - - public List afterInsertHandlers() { - if (this.handlerOrOrchestrator instanceof TriggerOrchestrator.AfterInsert) { - return ((TriggerOrchestrator.AfterInsert) this.handlerOrOrchestrator).afterInsertHandlers(); - } - - return new List{ (AfterInsert.Handler) this.handlerOrOrchestrator }; - } - } -} diff --git a/package/main/default/classes/AfterInsert.cls-meta.xml b/package/main/default/classes/AfterInsert.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/AfterInsert.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/AfterInsertTest.cls b/package/main/default/classes/AfterInsertTest.cls deleted file mode 100644 index eed6bc4..0000000 --- a/package/main/default/classes/AfterInsertTest.cls +++ /dev/null @@ -1,893 +0,0 @@ -/** - * @description Tests for the AfterInsert context of the TriggerOrchestrator class. - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class - * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexUnitTestClassShouldHaveRunAs: The lib does not branch on the running user, so running as one proves nothing - **/ -@SuppressWarnings('PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexUnitTestClassShouldHaveRunAs') -@IsTest(IsParallel=true testFor='ApexClass:AfterInsert, ApexClass:TriggerOrchestrator') -private class AfterInsertTest { - @IsTest - static void writerWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriter.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The writer should register the account.'); - } - - @IsTest - static void dispatcherWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterInsertFor(IdDispatcher.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The dispatcher should receive the qualified records.'); - } - - @IsTest - static void writerWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriterWithBypassable.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithBypassable()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The handler should not run.'); - } - - @IsTest - static void dispatcherWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - IdDispatcherWithBypassable dispatcher = new IdDispatcherWithBypassable(); - - TriggerOrchestrator.mock().afterInsertFor(IdDispatcherWithBypassable.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.isTrue(dispatcher.dispatchedIds.isEmpty(), 'The handler should not run.'); - } - - @IsTest - static void writer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriter.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The writer should register the account.'); - } - - @IsTest - static void writerWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriter.class).with(new Contact(LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The writer should not register an account.'); - } - - @IsTest - static void dispatcher() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterInsertFor(IdDispatcher.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The dispatcher should receive the qualified records.'); - } - - @IsTest - static void dispatcherWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - BatchDispatcher dispatcher = new BatchDispatcher(); - - TriggerOrchestrator.mock().afterInsertFor(BatchDispatcher.class).with(new Contact(LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(0, dispatcher.dispatchedBatches.size(), 'The dispatcher should not be called.'); - } - - @IsTest - static void dispatcherWithMultipleRecords() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact johnDoe = new Contact(FirstName = 'John', LastName = 'Doe'); - Contact janeDoe = new Contact(FirstName = 'Jane', LastName = 'Doe'); - - BatchDispatcher dispatcher = new BatchDispatcher(); - - TriggerOrchestrator.mock().afterInsertFor(BatchDispatcher.class).with(new List{ johnDoe, janeDoe }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List>{ new Set{ johnDoe.Id, janeDoe.Id } }, dispatcher.dispatchedBatches, 'The dispatcher should receive all records in one call.'); - } - - @IsTest - static void dispatcherBeforeFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - IdDispatcherWithFinalizerSnapshot dispatcher = new IdDispatcherWithFinalizerSnapshot(); - - TriggerOrchestrator.mock().afterInsertFor(IdDispatcherWithFinalizerSnapshot.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIdsWhenFinalized, 'The dispatcher should dispatch before it finalizes.'); - } - - @IsTest - static void writerWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - AccountWriterWithFinalizer writer = new AccountWriterWithFinalizer(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriterWithFinalizer.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.areEqual(new Set{ doe.Id }, writer.finalizedIds, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void dispatcherWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - IdDispatcherWithFinalizer dispatcher = new IdDispatcherWithFinalizer(); - - TriggerOrchestrator.mock().afterInsertFor(IdDispatcherWithFinalizer.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new Set{ doe.Id }, dispatcher.finalizedIds, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void writerWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - AccountWriterWithFinalizer writer = new AccountWriterWithFinalizer(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriterWithFinalizer.class).with(new Contact(LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.isNull(writer.finalizedIds, 'The finalizer should not run.'); - } - - @IsTest - static void dispatcherWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - IdDispatcherWithFinalizer dispatcher = new IdDispatcherWithFinalizer(); - - TriggerOrchestrator.mock().afterInsertFor(IdDispatcherWithFinalizer.class).with(new Contact(LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.isNull(dispatcher.finalizedIds, 'The finalizer should not run.'); - } - - @IsTest - static void writerWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().afterInsertFor(FailingWriter.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingWriter()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The writer failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void dispatcherWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().afterInsertFor(FailingDispatcher.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDispatcher()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The dispatcher failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void writerWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().afterInsertFor(FailingWriterWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingWriterWithContinueOnError()); - - // Verify - Assert.areEqual('The writer failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void dispatcherWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().afterInsertFor(FailingDispatcherWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingDispatcherWithContinueOnError()); - - // Verify - Assert.areEqual('The dispatcher failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenRetryIsRequested() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().afterInsertFor(RetryingWriterWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new RetryingWriterWithContinueOnError()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.isInstanceOfType(expectedException, EventBus.RetryableException.class, 'The retry request should not be swallowed.'); - } - - @IsTest - static void writerWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - TriggerOrchestrator.mock().afterInsertFor(DmlAccountWriter.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new DmlAccountWriter()); - - // Verify - Assert.areEqual(1, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The writer should register the account.'); - } - - @IsTest - static void parentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - SOQL.mock('SavedTriggerRecords').thenReturn(new Contact(Id = doe.Id, Account = acme)); - - TriggerOrchestrator.mock().afterInsertFor(AccountNameWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The writer should copy the account name.'); - } - - @IsTest - static void relatedQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - TriggerOrchestrator.mock().afterInsertFor(RelatedAccountNameWriter.class).with(new Contact(LastName = 'Doe', AccountId = acme.Id)); - - // Test - TriggerOrchestrator.runTestFor(new RelatedAccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The writer should copy the related account name.'); - } - - @IsTest - static void writerWithSharedUnitOfWorkWhenNextHandlerRuns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - CommitCountingDispatcher dispatcher = new CommitCountingDispatcher(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterInsert.add(new AccountWriter()); - handlers.afterInsert.add(dispatcher); - - TriggerOrchestrator.mock().afterInsertFor(TestOrchestrator.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(0, dispatcher.committedAccounts, 'The shared unit of work should not commit before the next handler.'); - } - - @IsTest - static void writerWithOwnUnitOfWork() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('ownUnit').allDmls(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriterWithOwnUnitOfWork.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithOwnUnitOfWork()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('ownUnit').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The own unit of work should commit the account.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenNextHandlerRuns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - CommitCountingDispatcher dispatcher = new CommitCountingDispatcher(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterInsert.add(new AccountWriterWithContinueOnError()); - handlers.afterInsert.add(dispatcher); - - TriggerOrchestrator.mock().afterInsertFor(TestOrchestrator.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(1, dispatcher.committedAccounts, 'The automatic unit of work should commit before the next handler.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenWriteFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - TriggerOrchestrator.mock().afterInsertFor(FailingAccountWriterWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingAccountWriterWithContinueOnError()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The automatic unit of work should not commit.'); - } - - @IsTest - static void writerWithOwnUnitOfWorkWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('ownUnit').allDmls(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriterWithPrefilledOwnUnitOfWork.class).with(new Contact(LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithPrefilledOwnUnitOfWork()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('ownUnit').insertsOf(Account.SObjectType).records().size(), 'The own unit of work should not commit.'); - } - - @IsTest - static void writerWithOwnUnitOfWorkAndContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('ownUnit').allDmls(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriterWithOwnUnitOfWorkAndContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithOwnUnitOfWorkAndContinueOnError()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('ownUnit').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The own unit of work should commit the account.'); - } - - @IsTest - static void writerWithSharedUnitOfWorkWhenCommitFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').exceptionOnInserts(); - - TriggerOrchestrator.mock().afterInsertFor(AccountWriter.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new AccountWriter()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.isInstanceOfType(expectedException, DmlException.class, 'The commit exception should propagate.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenCommitFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').exceptionOnInserts(); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().afterInsertFor(AccountWriterWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithContinueOnError()); - - // Verify - Assert.isInstanceOfType(logger.errors[0].getException(), DmlException.class, 'The commit exception should be logged.'); - } - - @IsTest - static void mockWith() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterInsertFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('AFTER_INSERT', doe.Description, 'The mocked context should run.'); - } - - @IsTest - static void mockWithPlatformEvent() { - // Setup - EventCountingDispatcher dispatcher = new EventCountingDispatcher(); - - TriggerOrchestrator.mock().afterInsertFor(EventCountingDispatcher.class).with(new BatchApexErrorEvent()); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(1, dispatcher.dispatchedEvents, 'The mocked event should be dispatched.'); - } - - @IsTest - static void mockWithParent() { - // Setup - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().afterInsertFor(AccountNameWriter.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The mocked parent should be served.'); - } - - @IsTest - static void mockWithParentWithoutRealQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().afterInsertFor(AccountNameWriter.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNameWriter()); - - // Verify - Assert.areEqual(0, Limits.getQueries(), 'The mocked context should not query the org.'); - } - - @IsTest - static void mockWithFakeId() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterInsertFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.isNotNull(doe.Id, 'The row should get a fake Id.'); - } - - private class OperationStamper implements AfterInsert.Writer { - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getNewSObject().put(Contact.Description, 'AFTER_INSERT'); - } - } - - private class EventCountingDispatcher implements AfterInsert.Dispatcher { - public Integer dispatchedEvents = 0; - - public Boolean dispatchOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return true; - } - - public void dispatchOnAfterInsert(TriggerTypes.InsertRecords records) { - this.dispatchedEvents += records.size(); - } - } - - private class AccountWriterWithBypassable extends AccountWriter implements AfterInsert.Bypassable { - public Boolean bypassOnAfterInsertWhen() { - return true; - } - } - - private class IdDispatcherWithBypassable extends IdDispatcher implements AfterInsert.Bypassable { - public Boolean bypassOnAfterInsertWhen() { - return true; - } - } - - private class BatchDispatcher implements AfterInsert.Dispatcher { - public List> dispatchedBatches = new List>(); - - public Boolean dispatchOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterInsert(TriggerTypes.InsertRecords records) { - this.dispatchedBatches.add(records.getIds()); - } - } - - private class IdDispatcherWithFinalizerSnapshot extends IdDispatcher implements AfterInsert.Finalizer { - public List dispatchedIdsWhenFinalized; - - public void finalizeOnAfterInsert(TriggerTypes.InsertRecords records) { - this.dispatchedIdsWhenFinalized = this.dispatchedIds.clone(); - } - } - - private class AccountWriterWithFinalizer extends AccountWriter implements AfterInsert.Finalizer { - public Set finalizedIds; - - public void finalizeOnAfterInsert(TriggerTypes.InsertRecords records) { - this.finalizedIds = records.getIds(); - } - } - - private class IdDispatcherWithFinalizer extends IdDispatcher implements AfterInsert.Finalizer { - public Set finalizedIds; - - public void finalizeOnAfterInsert(TriggerTypes.InsertRecords records) { - this.finalizedIds = records.getIds(); - } - } - - private virtual class FailingWriter implements AfterInsert.Writer { - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - throw new TestException('The writer failed.'); - } - } - - private class FailingWriterWithContinueOnError extends FailingWriter implements AfterInsert.ContinueOnError { - } - - private virtual class FailingDispatcher implements AfterInsert.Dispatcher { - public Boolean dispatchOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterInsert(TriggerTypes.InsertRecords records) { - throw new TestException('The dispatcher failed.'); - } - } - - private class FailingDispatcherWithContinueOnError extends FailingDispatcher implements AfterInsert.ContinueOnError { - } - - private class RetryingWriterWithContinueOnError implements AfterInsert.Writer, AfterInsert.ContinueOnError { - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - throw new EventBus.RetryableException('The writer asked for a retry.'); - } - } - - private class DmlAccountWriter implements AfterInsert.Writer { - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - Database.setSavepoint(); - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - } - } - - private virtual class AccountNameWriter implements AfterInsert.Writer, AfterInsert.ParentQuery { - public Map queryParentsOnAfterInsert() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Task(Subject = (String) record.getNewParent(Contact.AccountId)?.get(Account.Name))); - } - } - - private class RelatedAccountProvider implements AfterInsert.RecordsProvider { - public List query(TriggerTypes.InsertRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class RelatedAccountNameWriter implements AfterInsert.Writer, AfterInsert.RelatedQuery { - public Map queryRelatedOnAfterInsert() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert( - new Task(Subject = (String) record.getRelated('accounts').getFirstWhereKeyEquals(record.getNewSObject().get(Contact.AccountId))?.get(Account.Name)) - ); - } - } - - private class CommitCountingDispatcher implements AfterInsert.Dispatcher { - public Integer committedAccounts; - - public Boolean dispatchOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterInsert(TriggerTypes.InsertRecords records) { - this.committedAccounts = DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(); - } - } - - private virtual class AccountWriterWithOwnUnitOfWork extends AccountWriter implements AfterInsert.OwnUnitOfWork { - public DML.Committable ownUnitOfWorkOnAfterInsert() { - return new DML().identifier('ownUnit'); - } - } - - private class AccountWriterWithOwnUnitOfWorkAndContinueOnError extends AccountWriterWithOwnUnitOfWork implements AfterInsert.ContinueOnError { - } - - private class AccountWriterWithPrefilledOwnUnitOfWork extends AccountWriter implements AfterInsert.OwnUnitOfWork { - public DML.Committable ownUnitOfWorkOnAfterInsert() { - return new DML().toInsert(new Account(Name = 'Acme')).identifier('ownUnit'); - } - } - - private class AccountWriterWithContinueOnError extends AccountWriter implements AfterInsert.ContinueOnError { - } - - private class FailingAccountWriterWithContinueOnError implements AfterInsert.Writer, AfterInsert.ContinueOnError { - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - throw new TestException('The writer failed.'); - } - } - - private class TestOrchestrator implements TriggerOrchestrator.AfterInsert { - public List afterInsert = new List(); - - public List afterInsertHandlers() { - return this.afterInsert; - } - } - - private virtual class AccountWriter implements AfterInsert.Writer { - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - } - } - - private virtual class IdDispatcher implements AfterInsert.Dispatcher { - public List dispatchedIds = new List(); - - public Boolean dispatchOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterInsert(TriggerTypes.InsertRecords records) { - this.dispatchedIds.addAll(records.getIds()); - } - } - - public class TestLogger implements TriggerOrchestrator.Logger { - public List errors = new List(); - public Integer flushCalls = 0; - - public void log(TriggerOrchestrator.Error error) { - this.errors.add(error); - } - - public void flush() { - this.flushCalls++; - } - } - - private class TestException extends Exception { - } -} diff --git a/package/main/default/classes/AfterInsertTest.cls-meta.xml b/package/main/default/classes/AfterInsertTest.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/AfterInsertTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/AfterUndelete.cls b/package/main/default/classes/AfterUndelete.cls deleted file mode 100644 index 8d4f1a3..0000000 --- a/package/main/default/classes/AfterUndelete.cls +++ /dev/null @@ -1,279 +0,0 @@ -global class AfterUndelete { - // Roles - pick one - - global interface Writer extends Handler { - Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record); - void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork); - } - - global interface Dispatcher extends Handler { - Boolean dispatchOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record); - void dispatchOnAfterUndelete(TriggerTypes.UndeleteRecords records); - } - - // Add-ons - pick as many as you need - - global interface ParentQuery { - Map queryParentsOnAfterUndelete(); - } - - global interface RelatedQuery { - Map queryRelatedOnAfterUndelete(); - } - - global interface RecordsProvider { - List query(TriggerTypes.UndeleteRecords records); - String keyOf(SObject row); - } - - global interface OwnUnitOfWork { - DML.Committable ownUnitOfWorkOnAfterUndelete(); - } - - global interface Bypassable { - Boolean bypassOnAfterUndeleteWhen(); - } - - global interface Finalizer { - void finalizeOnAfterUndelete(TriggerTypes.UndeleteRecords records); - } - - global interface ContinueOnError { - } - - // Marker - - global interface Handler { - } - - // Implementation - - public class Operation extends TriggerOrchestrator.Operation { - public override List buildExecutors(TriggerOrchestrator orchestrator) { - List executors = new List(); - - for (AfterUndelete.Handler handler : ((TriggerOrchestrator.AfterUndelete) orchestrator.orchestrator).afterUndeleteHandlers()) { - if (handler instanceof AfterUndelete.Writer) { - executors.add(new AfterUndelete.WriterExecutor((AfterUndelete.Writer) handler, orchestrator)); - } else if (handler instanceof AfterUndelete.Dispatcher) { - executors.add(new AfterUndelete.DispatcherExecutor((AfterUndelete.Dispatcher) handler, orchestrator)); - } - } - - return executors; - } - } - - public class WriterExecutor extends TriggerOrchestrator.HandlerExecutor { - private AfterUndelete.Writer writer; - private TriggerOrchestrator.UnitOfWork unitOfWork; - - public WriterExecutor(AfterUndelete.Writer writer, TriggerOrchestrator orchestrator) { - super(writer, orchestrator); - - this.writer = writer; - this.parentDeclaration = new AfterUndelete.ParentDeclaration(this.writer); - this.relatedLoader = new AfterUndelete.RelatedLoader(this.writer); - this.unitOfWork = new AfterUndelete.UnitOfWorkResolver(this.writer).resolve(this.orchestrator.sharedUnitOfWork); - this.handlerHooks = new AfterUndelete.HandlerHooks(this.writer); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.write(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - this.commitOwnUnitOfWork(); - } catch (Exception e) { - this.handleException(e); - } - } - - private void write(TriggerTypes.TriggerRecord record) { - if (this.writer.writeOnAfterUndeleteWhen(record)) { - this.qualifiedRecords.add(record); - this.writer.writeOnAfterUndelete(record, this.unitOfWork); - } - } - - private void commitOwnUnitOfWork() { - if (this.qualifiedRecords.isEmpty() || this.unitOfWork === this.orchestrator.sharedUnitOfWork) { - return; - } - - this.unitOfWork.commitWork(); - } - } - - public class DispatcherExecutor extends TriggerOrchestrator.HandlerExecutor { - private AfterUndelete.Dispatcher dispatcher; - - public DispatcherExecutor(AfterUndelete.Dispatcher dispatcher, TriggerOrchestrator orchestrator) { - super(dispatcher, orchestrator); - - this.dispatcher = dispatcher; - this.parentDeclaration = new AfterUndelete.ParentDeclaration(this.dispatcher); - this.relatedLoader = new AfterUndelete.RelatedLoader(this.dispatcher); - this.handlerHooks = new AfterUndelete.HandlerHooks(this.dispatcher); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.qualify(record); - } - - this.dispatch(); - this.handlerHooks.finalize(this.qualifiedRecords); - } catch (Exception e) { - this.handleException(e); - } - } - - private void qualify(TriggerTypes.TriggerRecord record) { - if (this.dispatcher.dispatchOnAfterUndeleteWhen(record)) { - this.qualifiedRecords.add(record); - } - } - - private void dispatch() { - if (this.qualifiedRecords.isEmpty()) { - return; - } - - this.dispatcher.dispatchOnAfterUndelete(new TriggerTypes.UndeleteTriggerRecords(this.qualifiedRecords)); - } - } - - public class ParentDeclaration extends TriggerOrchestrator.ParentDeclaration { - private AfterUndelete.Handler handler; - - public ParentDeclaration(AfterUndelete.Handler handler) { - this.handler = handler; - } - - public override Map getNewParentFields() { - if (this.handler instanceof AfterUndelete.ParentQuery) { - return ((AfterUndelete.ParentQuery) this.handler).queryParentsOnAfterUndelete(); - } - - return new Map(); - } - } - - public class RelatedLoader extends TriggerOrchestrator.RelatedLoader { - private AfterUndelete.Handler handler; - private Map providers; - - public RelatedLoader(AfterUndelete.Handler handler) { - this.handler = handler; - } - - public override TriggerTypes.RelatedRecords queryRelated(String providerName) { - if (!(this.handler instanceof AfterUndelete.RelatedQuery)) { - return null; - } - - if (this.providers == null) { - this.providers = ((AfterUndelete.RelatedQuery) this.handler).queryRelatedOnAfterUndelete() ?? new Map(); - } - - AfterUndelete.RecordsProvider provider = this.providers.get(providerName); - - if (provider == null) { - return null; - } - - TriggerTypes.UndeleteRecords undeleteRecords = new TriggerTypes.UndeleteTriggerRecords(this.records); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(provider.query(undeleteRecords)); - - for (SObject row : related.getRecords()) { - related.groupUnderKey(provider.keyOf(row), row); - } - - return related; - } - } - - public class UnitOfWorkResolver extends TriggerOrchestrator.UnitOfWorkResolver { - private AfterUndelete.Writer writer; - - public UnitOfWorkResolver(AfterUndelete.Writer writer) { - this.writer = writer; - } - - public override TriggerOrchestrator.UnitOfWork resolve(TriggerOrchestrator.UnitOfWork sharedUnitOfWork) { - if (this.writer instanceof AfterUndelete.OwnUnitOfWork) { - return new TriggerOrchestrator.UnitOfWork(((AfterUndelete.OwnUnitOfWork) this.writer).ownUnitOfWorkOnAfterUndelete()); - } - - if (this.writer instanceof AfterUndelete.ContinueOnError) { - return new TriggerOrchestrator.UnitOfWork(); - } - - return sharedUnitOfWork; - } - } - - public class HandlerHooks extends TriggerOrchestrator.HandlerHooks { - private AfterUndelete.Handler handler; - - public HandlerHooks(AfterUndelete.Handler handler) { - this.handler = handler; - } - - public override Boolean isBypassedByAddOn() { - return (this.handler instanceof AfterUndelete.Bypassable && ((AfterUndelete.Bypassable) this.handler).bypassOnAfterUndeleteWhen()); - } - - public override void finalize(List qualifiedRecords) { - if (qualifiedRecords.isEmpty() || !(this.handler instanceof AfterUndelete.Finalizer)) { - return; - } - - ((AfterUndelete.Finalizer) this.handler).finalizeOnAfterUndelete(new TriggerTypes.UndeleteTriggerRecords(qualifiedRecords)); - } - - public override Boolean continuesOnError() { - return this.handler instanceof AfterUndelete.ContinueOnError; - } - } - - public class MockedContext extends TriggerOrchestrator.MockedContext implements TriggerOrchestrator.AfterUndeleteMockable, TriggerOrchestrator.AfterUndelete { - public MockedContext() { - super(System.TriggerOperation.AFTER_UNDELETE); - } - - public TriggerOrchestrator.AfterUndeleteMockable with(SObject row) { - return this.with(new List{ row }); - } - - public TriggerOrchestrator.AfterUndeleteMockable with(List rows) { - this.newRecords.addAll(this.assignIds(rows)); - return this; - } - - public TriggerOrchestrator.AfterUndeleteMockable withParent(SObjectField lookupField, SObject parent) { - return this.withParent(lookupField, new List{ parent }); - } - - public TriggerOrchestrator.AfterUndeleteMockable withParent(SObjectField lookupField, List parents) { - this.addParents(lookupField, parents); - return this; - } - - public List afterUndeleteHandlers() { - if (this.handlerOrOrchestrator instanceof TriggerOrchestrator.AfterUndelete) { - return ((TriggerOrchestrator.AfterUndelete) this.handlerOrOrchestrator).afterUndeleteHandlers(); - } - - return new List{ (AfterUndelete.Handler) this.handlerOrOrchestrator }; - } - } -} diff --git a/package/main/default/classes/AfterUndelete.cls-meta.xml b/package/main/default/classes/AfterUndelete.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/AfterUndelete.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/AfterUndeleteTest.cls b/package/main/default/classes/AfterUndeleteTest.cls deleted file mode 100644 index 7c166d1..0000000 --- a/package/main/default/classes/AfterUndeleteTest.cls +++ /dev/null @@ -1,720 +0,0 @@ -/** - * @description Tests for the AfterUndelete context of the TriggerOrchestrator class. - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class - * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexUnitTestClassShouldHaveRunAs: The lib does not branch on the running user, so running as one proves nothing - **/ -@SuppressWarnings('PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexUnitTestClassShouldHaveRunAs') -@IsTest(IsParallel=true testFor='ApexClass:AfterUndelete, ApexClass:TriggerOrchestrator') -private class AfterUndeleteTest { - @IsTest - static void writerWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUndeleteFor(AccountWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The writer should register the account.'); - } - - @IsTest - static void dispatcherWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterUndeleteFor(IdDispatcher.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The dispatcher should receive the qualified records.'); - } - - @IsTest - static void writerWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUndeleteFor(AccountWriterWithBypassable.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithBypassable()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The handler should not run.'); - } - - @IsTest - static void dispatcherWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - IdDispatcherWithBypassable dispatcher = new IdDispatcherWithBypassable(); - - TriggerOrchestrator.mock().afterUndeleteFor(IdDispatcherWithBypassable.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.isTrue(dispatcher.dispatchedIds.isEmpty(), 'The handler should not run.'); - } - - @IsTest - static void writer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUndeleteFor(AccountWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The writer should register the account.'); - } - - @IsTest - static void writerWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact roe = new Contact(LastName = 'Roe'); - - TriggerOrchestrator.mock().afterUndeleteFor(AccountWriter.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The writer should not register an account.'); - } - - @IsTest - static void dispatcher() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterUndeleteFor(IdDispatcher.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The dispatcher should receive the qualified records.'); - } - - @IsTest - static void dispatcherWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(LastName = 'Roe'); - - BatchDispatcher dispatcher = new BatchDispatcher(); - - TriggerOrchestrator.mock().afterUndeleteFor(BatchDispatcher.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(0, dispatcher.dispatchedBatches.size(), 'The dispatcher should not be called.'); - } - - @IsTest - static void dispatcherWithMultipleRecords() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact johnDoe = new Contact(FirstName = 'John', LastName = 'Doe'); - Contact janeDoe = new Contact(FirstName = 'Jane', LastName = 'Doe'); - - BatchDispatcher dispatcher = new BatchDispatcher(); - - TriggerOrchestrator.mock().afterUndeleteFor(BatchDispatcher.class).with(new List{ johnDoe, janeDoe }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List>{ new Set{ johnDoe.Id, janeDoe.Id } }, dispatcher.dispatchedBatches, 'The dispatcher should receive all records in one call.'); - } - - @IsTest - static void dispatcherBeforeFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - IdDispatcherWithFinalizerSnapshot dispatcher = new IdDispatcherWithFinalizerSnapshot(); - - TriggerOrchestrator.mock().afterUndeleteFor(IdDispatcherWithFinalizerSnapshot.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIdsWhenFinalized, 'The dispatcher should dispatch before it finalizes.'); - } - - @IsTest - static void writerWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - AccountWriterWithFinalizer writer = new AccountWriterWithFinalizer(); - - TriggerOrchestrator.mock().afterUndeleteFor(AccountWriterWithFinalizer.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.areEqual(new Set{ doe.Id }, writer.finalizedIds, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void dispatcherWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - IdDispatcherWithFinalizer dispatcher = new IdDispatcherWithFinalizer(); - - TriggerOrchestrator.mock().afterUndeleteFor(IdDispatcherWithFinalizer.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new Set{ doe.Id }, dispatcher.finalizedIds, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void writerWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(LastName = 'Roe'); - - AccountWriterWithFinalizer writer = new AccountWriterWithFinalizer(); - - TriggerOrchestrator.mock().afterUndeleteFor(AccountWriterWithFinalizer.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.isNull(writer.finalizedIds, 'The finalizer should not run.'); - } - - @IsTest - static void dispatcherWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(LastName = 'Roe'); - - IdDispatcherWithFinalizer dispatcher = new IdDispatcherWithFinalizer(); - - TriggerOrchestrator.mock().afterUndeleteFor(IdDispatcherWithFinalizer.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.isNull(dispatcher.finalizedIds, 'The finalizer should not run.'); - } - - @IsTest - static void writerWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().afterUndeleteFor(FailingWriter.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingWriter()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The writer failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void dispatcherWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().afterUndeleteFor(FailingDispatcher.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDispatcher()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The dispatcher failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void writerWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().afterUndeleteFor(FailingWriterWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingWriterWithContinueOnError()); - - // Verify - Assert.areEqual('The writer failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void dispatcherWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().afterUndeleteFor(FailingDispatcherWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingDispatcherWithContinueOnError()); - - // Verify - Assert.areEqual('The dispatcher failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void parentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - SOQL.mock('SavedTriggerRecords').thenReturn(new Contact(Id = doe.Id, Account = acme)); - - TriggerOrchestrator.mock().afterUndeleteFor(AccountNameWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The writer should copy the account name.'); - } - - @IsTest - static void relatedQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().afterUndeleteFor(RelatedAccountNameWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new RelatedAccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The writer should copy the related account name.'); - } - - @IsTest - static void writerWithSharedUnitOfWorkWhenNextHandlerRuns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - CommitCountingDispatcher dispatcher = new CommitCountingDispatcher(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterUndelete.add(new AccountWriter()); - handlers.afterUndelete.add(dispatcher); - - TriggerOrchestrator.mock().afterUndeleteFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(0, dispatcher.committedAccounts, 'The shared unit of work should not commit before the next handler.'); - } - - @IsTest - static void writerWithOwnUnitOfWork() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('ownUnit').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUndeleteFor(AccountWriterWithOwnUnitOfWork.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithOwnUnitOfWork()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('ownUnit').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The own unit of work should commit the account.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenNextHandlerRuns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - CommitCountingDispatcher dispatcher = new CommitCountingDispatcher(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterUndelete.add(new AccountWriterWithContinueOnError()); - handlers.afterUndelete.add(dispatcher); - - TriggerOrchestrator.mock().afterUndeleteFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(1, dispatcher.committedAccounts, 'The automatic unit of work should commit before the next handler.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenWriteFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUndeleteFor(FailingAccountWriterWithContinueOnError.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new FailingAccountWriterWithContinueOnError()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The automatic unit of work should not commit.'); - } - - @IsTest - static void mockWith() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUndeleteFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('AFTER_UNDELETE', doe.Description, 'The mocked context should run.'); - } - - @IsTest - static void mockWithParent() { - // Setup - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().afterUndeleteFor(AccountNameWriter.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The mocked parent should be served.'); - } - - private class OperationStamper implements AfterUndelete.Writer { - public Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getNewSObject().put(Contact.Description, 'AFTER_UNDELETE'); - } - } - - private class AccountWriterWithBypassable extends AccountWriter implements AfterUndelete.Bypassable { - public Boolean bypassOnAfterUndeleteWhen() { - return true; - } - } - - private class IdDispatcherWithBypassable extends IdDispatcher implements AfterUndelete.Bypassable { - public Boolean bypassOnAfterUndeleteWhen() { - return true; - } - } - - private class BatchDispatcher implements AfterUndelete.Dispatcher { - public List> dispatchedBatches = new List>(); - - public Boolean dispatchOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUndelete(TriggerTypes.UndeleteRecords records) { - this.dispatchedBatches.add(records.getIds()); - } - } - - private class IdDispatcherWithFinalizerSnapshot extends IdDispatcher implements AfterUndelete.Finalizer { - public List dispatchedIdsWhenFinalized; - - public void finalizeOnAfterUndelete(TriggerTypes.UndeleteRecords records) { - this.dispatchedIdsWhenFinalized = this.dispatchedIds.clone(); - } - } - - private class AccountWriterWithFinalizer extends AccountWriter implements AfterUndelete.Finalizer { - public Set finalizedIds; - - public void finalizeOnAfterUndelete(TriggerTypes.UndeleteRecords records) { - this.finalizedIds = records.getIds(); - } - } - - private class IdDispatcherWithFinalizer extends IdDispatcher implements AfterUndelete.Finalizer { - public Set finalizedIds; - - public void finalizeOnAfterUndelete(TriggerTypes.UndeleteRecords records) { - this.finalizedIds = records.getIds(); - } - } - - private virtual class FailingWriter implements AfterUndelete.Writer { - public Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - throw new TestException('The writer failed.'); - } - } - - private class FailingWriterWithContinueOnError extends FailingWriter implements AfterUndelete.ContinueOnError { - } - - private virtual class FailingDispatcher implements AfterUndelete.Dispatcher { - public Boolean dispatchOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUndelete(TriggerTypes.UndeleteRecords records) { - throw new TestException('The dispatcher failed.'); - } - } - - private class FailingDispatcherWithContinueOnError extends FailingDispatcher implements AfterUndelete.ContinueOnError { - } - - private virtual class AccountNameWriter implements AfterUndelete.Writer, AfterUndelete.ParentQuery { - public Map queryParentsOnAfterUndelete() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Task(Subject = (String) record.getNewParent(Contact.AccountId)?.get(Account.Name))); - } - } - - private class RelatedAccountProvider implements AfterUndelete.RecordsProvider { - public List query(TriggerTypes.UndeleteRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class RelatedAccountNameWriter implements AfterUndelete.Writer, AfterUndelete.RelatedQuery { - public Map queryRelatedOnAfterUndelete() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert( - new Task(Subject = (String) record.getRelated('accounts').getFirstWhereKeyEquals(record.getNewSObject().get(Contact.AccountId))?.get(Account.Name)) - ); - } - } - - private class CommitCountingDispatcher implements AfterUndelete.Dispatcher { - public Integer committedAccounts; - - public Boolean dispatchOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUndelete(TriggerTypes.UndeleteRecords records) { - this.committedAccounts = DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(); - } - } - - private virtual class AccountWriterWithOwnUnitOfWork extends AccountWriter implements AfterUndelete.OwnUnitOfWork { - public DML.Committable ownUnitOfWorkOnAfterUndelete() { - return new DML().identifier('ownUnit'); - } - } - - private class AccountWriterWithContinueOnError extends AccountWriter implements AfterUndelete.ContinueOnError { - } - - private class FailingAccountWriterWithContinueOnError implements AfterUndelete.Writer, AfterUndelete.ContinueOnError { - public Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - throw new TestException('The writer failed.'); - } - } - - private class TestOrchestrator implements TriggerOrchestrator.AfterUndelete { - public List afterUndelete = new List(); - - public List afterUndeleteHandlers() { - return this.afterUndelete; - } - } - - private virtual class AccountWriter implements AfterUndelete.Writer { - public Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - } - } - - private virtual class IdDispatcher implements AfterUndelete.Dispatcher { - public List dispatchedIds = new List(); - - public Boolean dispatchOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUndelete(TriggerTypes.UndeleteRecords records) { - this.dispatchedIds.addAll(records.getIds()); - } - } - - public class TestLogger implements TriggerOrchestrator.Logger { - public List errors = new List(); - public Integer flushCalls = 0; - - public void log(TriggerOrchestrator.Error error) { - this.errors.add(error); - } - - public void flush() { - this.flushCalls++; - } - } - - private class TestException extends Exception { - } -} diff --git a/package/main/default/classes/AfterUndeleteTest.cls-meta.xml b/package/main/default/classes/AfterUndeleteTest.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/AfterUndeleteTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/AfterUpdate.cls b/package/main/default/classes/AfterUpdate.cls deleted file mode 100644 index 1623967..0000000 --- a/package/main/default/classes/AfterUpdate.cls +++ /dev/null @@ -1,313 +0,0 @@ -global class AfterUpdate { - // Roles - pick one - - global interface Writer extends Handler { - Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record); - void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork); - } - - global interface Dispatcher extends Handler { - Boolean dispatchOnAfterUpdateWhen(TriggerTypes.UpdateRecord record); - void dispatchOnAfterUpdate(TriggerTypes.UpdateRecords records); - } - - // Add-ons - pick as many as you need - - global interface ParentQuery { - Map queryParentsOnAfterUpdate(); - } - - global interface PriorParentQuery { - Map queryPriorParentsOnAfterUpdate(); - } - - global interface RelatedQuery { - Map queryRelatedOnAfterUpdate(); - } - - global interface RecordsProvider { - List query(TriggerTypes.UpdateRecords records); - String keyOf(SObject row); - } - - global interface OwnUnitOfWork { - DML.Committable ownUnitOfWorkOnAfterUpdate(); - } - - global interface Bypassable { - Boolean bypassOnAfterUpdateWhen(); - } - - global interface RecursionGuard { - Integer maxRunsPerRecordOnAfterUpdate(); - } - - global interface Finalizer { - void finalizeOnAfterUpdate(TriggerTypes.UpdateRecords records); - } - - global interface ContinueOnError { - } - - // Marker - - global interface Handler { - } - - // Implementation - - public class Operation extends TriggerOrchestrator.Operation { - public override List buildExecutors(TriggerOrchestrator orchestrator) { - List executors = new List(); - - for (AfterUpdate.Handler handler : ((TriggerOrchestrator.AfterUpdate) orchestrator.orchestrator).afterUpdateHandlers()) { - if (handler instanceof AfterUpdate.Writer) { - executors.add(new AfterUpdate.WriterExecutor((AfterUpdate.Writer) handler, orchestrator)); - } else if (handler instanceof AfterUpdate.Dispatcher) { - executors.add(new AfterUpdate.DispatcherExecutor((AfterUpdate.Dispatcher) handler, orchestrator)); - } - } - - return executors; - } - } - - public class WriterExecutor extends TriggerOrchestrator.HandlerExecutor { - private AfterUpdate.Writer writer; - private TriggerOrchestrator.UnitOfWork unitOfWork; - private TriggerOrchestrator.RecursionTracker recursionTracker; - - public WriterExecutor(AfterUpdate.Writer writer, TriggerOrchestrator orchestrator) { - super(writer, orchestrator); - - this.writer = writer; - this.parentDeclaration = new AfterUpdate.ParentDeclaration(this.writer); - this.relatedLoader = new AfterUpdate.RelatedLoader(this.writer); - this.unitOfWork = new AfterUpdate.UnitOfWorkResolver(this.writer).resolve(this.orchestrator.sharedUnitOfWork); - this.handlerHooks = new AfterUpdate.HandlerHooks(this.writer); - this.recursionTracker = new AfterUpdate.RecursionTracker(this.writer, this.handlerName); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.write(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - this.commitOwnUnitOfWork(); - } catch (Exception e) { - this.handleException(e); - } - } - - private void write(TriggerTypes.TriggerRecord record) { - if (!this.recursionTracker.isLimitReached(record) && this.writer.writeOnAfterUpdateWhen(record)) { - this.recursionTracker.countRun(record); - this.qualifiedRecords.add(record); - this.writer.writeOnAfterUpdate(record, this.unitOfWork); - } - } - - private void commitOwnUnitOfWork() { - if (this.qualifiedRecords.isEmpty() || this.unitOfWork === this.orchestrator.sharedUnitOfWork) { - return; - } - - this.unitOfWork.commitWork(); - } - } - - public class DispatcherExecutor extends TriggerOrchestrator.HandlerExecutor { - private AfterUpdate.Dispatcher dispatcher; - private TriggerOrchestrator.RecursionTracker recursionTracker; - - public DispatcherExecutor(AfterUpdate.Dispatcher dispatcher, TriggerOrchestrator orchestrator) { - super(dispatcher, orchestrator); - - this.dispatcher = dispatcher; - this.parentDeclaration = new AfterUpdate.ParentDeclaration(this.dispatcher); - this.relatedLoader = new AfterUpdate.RelatedLoader(this.dispatcher); - this.handlerHooks = new AfterUpdate.HandlerHooks(this.dispatcher); - this.recursionTracker = new AfterUpdate.RecursionTracker(this.dispatcher, this.handlerName); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.qualify(record); - } - - this.dispatch(); - this.handlerHooks.finalize(this.qualifiedRecords); - } catch (Exception e) { - this.handleException(e); - } - } - - private void qualify(TriggerTypes.TriggerRecord record) { - if (!this.recursionTracker.isLimitReached(record) && this.dispatcher.dispatchOnAfterUpdateWhen(record)) { - this.recursionTracker.countRun(record); - this.qualifiedRecords.add(record); - } - } - - private void dispatch() { - if (this.qualifiedRecords.isEmpty()) { - return; - } - - this.dispatcher.dispatchOnAfterUpdate(new TriggerTypes.UpdateTriggerRecords(this.qualifiedRecords)); - } - } - - public class ParentDeclaration extends TriggerOrchestrator.ParentDeclaration { - private AfterUpdate.Handler handler; - - public ParentDeclaration(AfterUpdate.Handler handler) { - this.handler = handler; - } - - public override Map getNewParentFields() { - if (this.handler instanceof AfterUpdate.ParentQuery) { - return ((AfterUpdate.ParentQuery) this.handler).queryParentsOnAfterUpdate(); - } - - return new Map(); - } - - public override Map getOldParentFields() { - if (this.handler instanceof AfterUpdate.PriorParentQuery) { - return ((AfterUpdate.PriorParentQuery) this.handler).queryPriorParentsOnAfterUpdate(); - } - - return new Map(); - } - } - - public class RelatedLoader extends TriggerOrchestrator.RelatedLoader { - private AfterUpdate.Handler handler; - private Map providers; - - public RelatedLoader(AfterUpdate.Handler handler) { - this.handler = handler; - } - - public override TriggerTypes.RelatedRecords queryRelated(String providerName) { - if (!(this.handler instanceof AfterUpdate.RelatedQuery)) { - return null; - } - - if (this.providers == null) { - this.providers = ((AfterUpdate.RelatedQuery) this.handler).queryRelatedOnAfterUpdate() ?? new Map(); - } - - AfterUpdate.RecordsProvider provider = this.providers.get(providerName); - - if (provider == null) { - return null; - } - - TriggerTypes.UpdateRecords updateRecords = new TriggerTypes.UpdateTriggerRecords(this.records); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(provider.query(updateRecords)); - - for (SObject row : related.getRecords()) { - related.groupUnderKey(provider.keyOf(row), row); - } - - return related; - } - } - - public class UnitOfWorkResolver extends TriggerOrchestrator.UnitOfWorkResolver { - private AfterUpdate.Writer writer; - - public UnitOfWorkResolver(AfterUpdate.Writer writer) { - this.writer = writer; - } - - public override TriggerOrchestrator.UnitOfWork resolve(TriggerOrchestrator.UnitOfWork sharedUnitOfWork) { - if (this.writer instanceof AfterUpdate.OwnUnitOfWork) { - return new TriggerOrchestrator.UnitOfWork(((AfterUpdate.OwnUnitOfWork) this.writer).ownUnitOfWorkOnAfterUpdate()); - } - - if (this.writer instanceof AfterUpdate.ContinueOnError) { - return new TriggerOrchestrator.UnitOfWork(); - } - - return sharedUnitOfWork; - } - } - - public class HandlerHooks extends TriggerOrchestrator.HandlerHooks { - private AfterUpdate.Handler handler; - - public HandlerHooks(AfterUpdate.Handler handler) { - this.handler = handler; - } - - public override Boolean isBypassedByAddOn() { - return (this.handler instanceof AfterUpdate.Bypassable && ((AfterUpdate.Bypassable) this.handler).bypassOnAfterUpdateWhen()); - } - - public override void finalize(List qualifiedRecords) { - if (qualifiedRecords.isEmpty() || !(this.handler instanceof AfterUpdate.Finalizer)) { - return; - } - - ((AfterUpdate.Finalizer) this.handler).finalizeOnAfterUpdate(new TriggerTypes.UpdateTriggerRecords(qualifiedRecords)); - } - - public override Boolean continuesOnError() { - return this.handler instanceof AfterUpdate.ContinueOnError; - } - } - - public class RecursionTracker extends TriggerOrchestrator.RecursionTracker { - public RecursionTracker(AfterUpdate.Handler handler, String handlerName) { - super(handlerName + ':AFTER_UPDATE'); - - if (handler instanceof AfterUpdate.RecursionGuard) { - this.maxRunsPerRecord = ((AfterUpdate.RecursionGuard) handler).maxRunsPerRecordOnAfterUpdate(); - } - } - } - - public class MockedContext extends TriggerOrchestrator.MockedContext implements TriggerOrchestrator.AfterUpdateMockable, TriggerOrchestrator.AfterUpdate { - public MockedContext() { - super(System.TriggerOperation.AFTER_UPDATE); - } - - public TriggerOrchestrator.AfterUpdateMockable with(SObject newRow, SObject oldRow) { - return this.with(new List{ newRow }, new List{ oldRow }); - } - - public TriggerOrchestrator.AfterUpdateMockable with(List newRows, List oldRows) { - this.shareIds(newRows, oldRows); - this.newRecords.addAll(newRows); - this.oldRecords.addAll(oldRows); - return this; - } - - public TriggerOrchestrator.AfterUpdateMockable withParent(SObjectField lookupField, SObject parent) { - return this.withParent(lookupField, new List{ parent }); - } - - public TriggerOrchestrator.AfterUpdateMockable withParent(SObjectField lookupField, List parents) { - this.addParents(lookupField, parents); - return this; - } - - public List afterUpdateHandlers() { - if (this.handlerOrOrchestrator instanceof TriggerOrchestrator.AfterUpdate) { - return ((TriggerOrchestrator.AfterUpdate) this.handlerOrOrchestrator).afterUpdateHandlers(); - } - - return new List{ (AfterUpdate.Handler) this.handlerOrOrchestrator }; - } - } -} diff --git a/package/main/default/classes/AfterUpdate.cls-meta.xml b/package/main/default/classes/AfterUpdate.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/AfterUpdate.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/AfterUpdateTest.cls b/package/main/default/classes/AfterUpdateTest.cls deleted file mode 100644 index 986fc3d..0000000 --- a/package/main/default/classes/AfterUpdateTest.cls +++ /dev/null @@ -1,1054 +0,0 @@ -/** - * @description Tests for the AfterUpdate context of the TriggerOrchestrator class. - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class - * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexUnitTestClassShouldHaveRunAs: The lib does not branch on the running user, so running as one proves nothing - **/ -@SuppressWarnings('PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexUnitTestClassShouldHaveRunAs') -@IsTest(IsParallel=true testFor='ApexClass:AfterUpdate, ApexClass:TriggerOrchestrator') -private class AfterUpdateTest { - @IsTest - static void writerWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(AccountWriter.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The writer should register the account.'); - } - - @IsTest - static void dispatcherWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterUpdateFor(IdDispatcher.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The dispatcher should receive the qualified records.'); - } - - @IsTest - static void writerWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(AccountWriterWithBypassable.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithBypassable()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The handler should not run.'); - } - - @IsTest - static void dispatcherWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcherWithBypassable dispatcher = new IdDispatcherWithBypassable(); - - TriggerOrchestrator.mock().afterUpdateFor(IdDispatcherWithBypassable.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.isTrue(dispatcher.dispatchedIds.isEmpty(), 'The handler should not run.'); - } - - @IsTest - static void writer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(AccountWriter.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The writer should register the account.'); - } - - @IsTest - static void writerWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - TriggerOrchestrator.mock().afterUpdateFor(AccountWriter.class).with(roe, new Contact(Id = roe.Id, LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The writer should not register an account.'); - } - - @IsTest - static void dispatcher() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock() - .afterUpdateFor(IdDispatcher.class) - .with(new List{ doe, roe }, new List{ new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(Id = roe.Id, LastName = 'Roe') }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The dispatcher should receive the qualified records.'); - } - - @IsTest - static void dispatcherWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - BatchDispatcher dispatcher = new BatchDispatcher(); - - TriggerOrchestrator.mock().afterUpdateFor(BatchDispatcher.class).with(roe, new Contact(Id = roe.Id, LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(0, dispatcher.dispatchedBatches.size(), 'The dispatcher should not be called.'); - } - - @IsTest - static void dispatcherWithMultipleRecords() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact johnDoe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), FirstName = 'John', LastName = 'Doe'); - Contact janeDoe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), FirstName = 'Jane', LastName = 'Doe'); - - BatchDispatcher dispatcher = new BatchDispatcher(); - - TriggerOrchestrator.mock() - .afterUpdateFor(BatchDispatcher.class) - .with( - new List{ johnDoe, janeDoe }, - new List{ new Contact(Id = johnDoe.Id, FirstName = 'John', LastName = 'Doe'), new Contact(Id = janeDoe.Id, FirstName = 'Jane', LastName = 'Doe') } - ); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List>{ new Set{ johnDoe.Id, janeDoe.Id } }, dispatcher.dispatchedBatches, 'The dispatcher should receive all records in one call.'); - } - - @IsTest - static void dispatcherBeforeFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcherWithFinalizerSnapshot dispatcher = new IdDispatcherWithFinalizerSnapshot(); - - TriggerOrchestrator.mock().afterUpdateFor(IdDispatcherWithFinalizerSnapshot.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIdsWhenFinalized, 'The dispatcher should dispatch before it finalizes.'); - } - - @IsTest - static void writerWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - AccountWriterWithFinalizer writer = new AccountWriterWithFinalizer(); - - TriggerOrchestrator.mock() - .afterUpdateFor(AccountWriterWithFinalizer.class) - .with(new List{ doe, roe }, new List{ new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(Id = roe.Id, LastName = 'Roe') }); - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.areEqual(new Set{ doe.Id }, writer.finalizedIds, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void dispatcherWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - IdDispatcherWithFinalizer dispatcher = new IdDispatcherWithFinalizer(); - - TriggerOrchestrator.mock() - .afterUpdateFor(IdDispatcherWithFinalizer.class) - .with(new List{ doe, roe }, new List{ new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(Id = roe.Id, LastName = 'Roe') }); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new Set{ doe.Id }, dispatcher.finalizedIds, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void writerWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - AccountWriterWithFinalizer writer = new AccountWriterWithFinalizer(); - - TriggerOrchestrator.mock().afterUpdateFor(AccountWriterWithFinalizer.class).with(roe, new Contact(Id = roe.Id, LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.isNull(writer.finalizedIds, 'The finalizer should not run.'); - } - - @IsTest - static void dispatcherWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - IdDispatcherWithFinalizer dispatcher = new IdDispatcherWithFinalizer(); - - TriggerOrchestrator.mock().afterUpdateFor(IdDispatcherWithFinalizer.class).with(roe, new Contact(Id = roe.Id, LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.isNull(dispatcher.finalizedIds, 'The finalizer should not run.'); - } - - @IsTest - static void writerWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(FailingWriter.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingWriter()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The writer failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void dispatcherWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(FailingDispatcher.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDispatcher()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The dispatcher failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void writerWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(FailingWriterWithContinueOnError.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingWriterWithContinueOnError()); - - // Verify - Assert.areEqual('The writer failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void dispatcherWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(FailingDispatcherWithContinueOnError.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingDispatcherWithContinueOnError()); - - // Verify - Assert.areEqual('The dispatcher failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void writerWithDefaultRecursionDepth() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(AccountWriter.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.runTestFor(new AccountWriter()); - } - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual(3, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The writer should not run beyond the recursion depth.'); - } - - @IsTest - static void dispatcherWithDefaultRecursionDepth() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterUpdateFor(IdDispatcher.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.runTestFor(dispatcher); - } - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(3, dispatcher.dispatchedIds.size(), 'The dispatcher should not run beyond the recursion depth.'); - } - - @IsTest - static void writerWithRecursionGuard() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(AccountWriterWithRecursionGuard.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - TriggerOrchestrator.runTestFor(new AccountWriterWithRecursionGuard()); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithRecursionGuard()); - - // Verify - Assert.areEqual(1, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The writer should not run beyond its recursion depth.'); - } - - @IsTest - static void dispatcherWithRecursionGuard() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcherWithRecursionGuard dispatcher = new IdDispatcherWithRecursionGuard(); - - TriggerOrchestrator.mock().afterUpdateFor(IdDispatcherWithRecursionGuard.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - TriggerOrchestrator.runTestFor(dispatcher); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(1, dispatcher.dispatchedIds.size(), 'The dispatcher should not run beyond its recursion depth.'); - } - - @IsTest - static void writerQualificationWhenRecursionDepthIsExceeded() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - QualificationCountingWriter writer = new QualificationCountingWriter(); - - TriggerOrchestrator.mock().afterUpdateFor(QualificationCountingWriter.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.runTestFor(writer); - } - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.areEqual(3, writer.qualificationCalls, 'The qualification should not be asked beyond the recursion depth.'); - } - - @IsTest - static void dispatcherQualificationWhenRecursionDepthIsExceeded() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - QualificationCountingDispatcher dispatcher = new QualificationCountingDispatcher(); - - TriggerOrchestrator.mock().afterUpdateFor(QualificationCountingDispatcher.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.runTestFor(dispatcher); - } - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(3, dispatcher.qualificationCalls, 'The qualification should not be asked beyond the recursion depth.'); - } - - @IsTest - static void writerWithRecursionGuardWhenEarlierRunDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock() - .afterUpdateFor(AccountWriterWithRecursionGuard.class) - .with(new Contact(Id = doe.Id, LastName = 'Roe'), new Contact(Id = doe.Id, LastName = 'Roe')); - TriggerOrchestrator.mock().afterUpdateFor(AccountWriterWithRecursionGuard.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - TriggerOrchestrator.runTestFor(new AccountWriterWithRecursionGuard()); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithRecursionGuard()); - - // Verify - Assert.areEqual(1, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The writer should register the account.'); - } - - @IsTest - static void dispatcherWithRecursionGuardWhenEarlierRunDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcherWithRecursionGuard dispatcher = new IdDispatcherWithRecursionGuard(); - - TriggerOrchestrator.mock() - .afterUpdateFor(IdDispatcherWithRecursionGuard.class) - .with(new Contact(Id = doe.Id, LastName = 'Roe'), new Contact(Id = doe.Id, LastName = 'Roe')); - TriggerOrchestrator.mock().afterUpdateFor(IdDispatcherWithRecursionGuard.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - TriggerOrchestrator.runTestFor(dispatcher); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The dispatcher should receive the qualified records.'); - } - - @IsTest - static void finalizerWhenRecursionDepthIsExceeded() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - FinalizeCountingWriter writer = new FinalizeCountingWriter(); - - TriggerOrchestrator.mock().afterUpdateFor(FinalizeCountingWriter.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.runTestFor(writer); - } - - // Test - TriggerOrchestrator.runTestFor(writer); - - // Verify - Assert.areEqual(3, writer.finalizeCalls, 'The finalizer should not run beyond the recursion depth.'); - } - - @IsTest - static void parentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - SOQL.mock('SavedTriggerRecords').thenReturn(new Contact(Id = doe.Id, Account = acme)); - - TriggerOrchestrator.mock().afterUpdateFor(AccountNameWriter.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The writer should copy the account name.'); - } - - @IsTest - static void priorParentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock() - .afterUpdateFor(PriorAccountNameWriter.class) - .with(doe, new Contact(Id = doe.Id, LastName = 'Doe', AccountId = acme.Id)) - .withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new PriorAccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The writer should copy the prior account name.'); - } - - @IsTest - static void priorParentQueryWithParentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - SOQL.mock('SavedTriggerRecords').thenReturn(new Contact(Id = doe.Id, Account = acme)); - - TriggerOrchestrator.mock().afterUpdateFor(AccountNameWriterWithPriorParentQuery.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe', AccountId = acme.Id)); - - // Test - Test.startTest(); - Integer queriesBefore = SOQL.getQueries(); - TriggerOrchestrator.runTestFor(new AccountNameWriterWithPriorParentQuery()); - Integer queriesAfter = SOQL.getQueries(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, queriesAfter - queriesBefore, 'The prior account should come from the saved records.'); - } - - @IsTest - static void relatedQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().afterUpdateFor(RelatedAccountNameWriter.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe', AccountId = acme.Id)); - - // Test - TriggerOrchestrator.runTestFor(new RelatedAccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The writer should copy the related account name.'); - } - - @IsTest - static void writerWithSharedUnitOfWorkWhenNextHandlerRuns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - CommitCountingDispatcher dispatcher = new CommitCountingDispatcher(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterUpdate.add(new AccountWriter()); - handlers.afterUpdate.add(dispatcher); - - TriggerOrchestrator.mock().afterUpdateFor(TestOrchestrator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(0, dispatcher.committedAccounts, 'The shared unit of work should not commit before the next handler.'); - } - - @IsTest - static void writerWithOwnUnitOfWork() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('ownUnit').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(AccountWriterWithOwnUnitOfWork.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithOwnUnitOfWork()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('ownUnit').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The own unit of work should commit the account.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenNextHandlerRuns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - CommitCountingDispatcher dispatcher = new CommitCountingDispatcher(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterUpdate.add(new AccountWriterWithContinueOnError()); - handlers.afterUpdate.add(dispatcher); - - TriggerOrchestrator.mock().afterUpdateFor(TestOrchestrator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(1, dispatcher.committedAccounts, 'The automatic unit of work should commit before the next handler.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenWriteFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(FailingAccountWriterWithContinueOnError.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingAccountWriterWithContinueOnError()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The automatic unit of work should not commit.'); - } - - @IsTest - static void mockWithPair() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterUpdateFor(OperationStamper.class).with(doe, new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('AFTER_UPDATE', doe.Description, 'The mocked context should run.'); - } - - @IsTest - static void mockWithParent() { - // Setup - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().afterUpdateFor(AccountNameWriter.class).with(doe, new Contact(LastName = 'Doe')).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNameWriter()); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Task.SObjectType).records()[0].get(Task.Subject), 'The mocked parent should be served.'); - } - - private class OperationStamper implements AfterUpdate.Writer { - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getNewSObject().put(Contact.Description, 'AFTER_UPDATE'); - } - } - - private class AccountWriterWithBypassable extends AccountWriter implements AfterUpdate.Bypassable { - public Boolean bypassOnAfterUpdateWhen() { - return true; - } - } - - private class IdDispatcherWithBypassable extends IdDispatcher implements AfterUpdate.Bypassable { - public Boolean bypassOnAfterUpdateWhen() { - return true; - } - } - - private class BatchDispatcher implements AfterUpdate.Dispatcher { - public List> dispatchedBatches = new List>(); - - public Boolean dispatchOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUpdate(TriggerTypes.UpdateRecords records) { - this.dispatchedBatches.add(records.getIds()); - } - } - - private class IdDispatcherWithFinalizerSnapshot extends IdDispatcher implements AfterUpdate.Finalizer { - public List dispatchedIdsWhenFinalized; - - public void finalizeOnAfterUpdate(TriggerTypes.UpdateRecords records) { - this.dispatchedIdsWhenFinalized = this.dispatchedIds.clone(); - } - } - - private class AccountWriterWithFinalizer extends AccountWriter implements AfterUpdate.Finalizer { - public Set finalizedIds; - - public void finalizeOnAfterUpdate(TriggerTypes.UpdateRecords records) { - this.finalizedIds = records.getIds(); - } - } - - private class IdDispatcherWithFinalizer extends IdDispatcher implements AfterUpdate.Finalizer { - public Set finalizedIds; - - public void finalizeOnAfterUpdate(TriggerTypes.UpdateRecords records) { - this.finalizedIds = records.getIds(); - } - } - - private virtual class FailingWriter implements AfterUpdate.Writer { - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - throw new TestException('The writer failed.'); - } - } - - private class FailingWriterWithContinueOnError extends FailingWriter implements AfterUpdate.ContinueOnError { - } - - private virtual class FailingDispatcher implements AfterUpdate.Dispatcher { - public Boolean dispatchOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUpdate(TriggerTypes.UpdateRecords records) { - throw new TestException('The dispatcher failed.'); - } - } - - private class FailingDispatcherWithContinueOnError extends FailingDispatcher implements AfterUpdate.ContinueOnError { - } - - private class AccountWriterWithRecursionGuard extends AccountWriter implements AfterUpdate.RecursionGuard { - public Integer maxRunsPerRecordOnAfterUpdate() { - return 1; - } - } - - private class IdDispatcherWithRecursionGuard extends IdDispatcher implements AfterUpdate.RecursionGuard { - public Integer maxRunsPerRecordOnAfterUpdate() { - return 1; - } - } - - private class QualificationCountingWriter implements AfterUpdate.Writer { - public Integer qualificationCalls = 0; - - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - this.qualificationCalls++; - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - } - } - - private class QualificationCountingDispatcher implements AfterUpdate.Dispatcher { - public Integer qualificationCalls = 0; - - public Boolean dispatchOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - this.qualificationCalls++; - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUpdate(TriggerTypes.UpdateRecords records) { - } - } - - private class FinalizeCountingWriter extends AccountWriter implements AfterUpdate.Finalizer { - public Integer finalizeCalls = 0; - - public void finalizeOnAfterUpdate(TriggerTypes.UpdateRecords records) { - this.finalizeCalls++; - } - } - - private virtual class AccountNameWriter implements AfterUpdate.Writer, AfterUpdate.ParentQuery { - public Map queryParentsOnAfterUpdate() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Task(Subject = (String) record.getNewParent(Contact.AccountId)?.get(Account.Name))); - } - } - - private class AccountNameWriterWithPriorParentQuery extends AccountNameWriter implements AfterUpdate.PriorParentQuery { - public Map queryPriorParentsOnAfterUpdate() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - } - - private class PriorAccountNameWriter implements AfterUpdate.Writer, AfterUpdate.PriorParentQuery { - public Map queryPriorParentsOnAfterUpdate() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Task(Subject = (String) record.getOldParent(Contact.AccountId)?.get(Account.Name))); - } - } - - private class RelatedAccountProvider implements AfterUpdate.RecordsProvider { - public List query(TriggerTypes.UpdateRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class RelatedAccountNameWriter implements AfterUpdate.Writer, AfterUpdate.RelatedQuery { - public Map queryRelatedOnAfterUpdate() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert( - new Task(Subject = (String) record.getRelated('accounts').getFirstWhereKeyEquals(record.getNewSObject().get(Contact.AccountId))?.get(Account.Name)) - ); - } - } - - private class CommitCountingDispatcher implements AfterUpdate.Dispatcher { - public Integer committedAccounts; - - public Boolean dispatchOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUpdate(TriggerTypes.UpdateRecords records) { - this.committedAccounts = DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(); - } - } - - private virtual class AccountWriterWithOwnUnitOfWork extends AccountWriter implements AfterUpdate.OwnUnitOfWork { - public DML.Committable ownUnitOfWorkOnAfterUpdate() { - return new DML().identifier('ownUnit'); - } - } - - private class AccountWriterWithContinueOnError extends AccountWriter implements AfterUpdate.ContinueOnError { - } - - private class FailingAccountWriterWithContinueOnError implements AfterUpdate.Writer, AfterUpdate.ContinueOnError { - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - throw new TestException('The writer failed.'); - } - } - - private class TestOrchestrator implements TriggerOrchestrator.AfterUpdate { - public List afterUpdate = new List(); - - public List afterUpdateHandlers() { - return this.afterUpdate; - } - } - - private virtual class AccountWriter implements AfterUpdate.Writer { - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - } - } - - private virtual class IdDispatcher implements AfterUpdate.Dispatcher { - public List dispatchedIds = new List(); - - public Boolean dispatchOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUpdate(TriggerTypes.UpdateRecords records) { - this.dispatchedIds.addAll(records.getIds()); - } - } - - public class TestLogger implements TriggerOrchestrator.Logger { - public List errors = new List(); - public Integer flushCalls = 0; - - public void log(TriggerOrchestrator.Error error) { - this.errors.add(error); - } - - public void flush() { - this.flushCalls++; - } - } - - private class TestException extends Exception { - } -} diff --git a/package/main/default/classes/AfterUpdateTest.cls-meta.xml b/package/main/default/classes/AfterUpdateTest.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/AfterUpdateTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/BeforeDelete.cls b/package/main/default/classes/BeforeDelete.cls deleted file mode 100644 index 0562fee..0000000 --- a/package/main/default/classes/BeforeDelete.cls +++ /dev/null @@ -1,280 +0,0 @@ -global class BeforeDelete { - // Roles - pick one - - global interface Validator extends Handler { - Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record); - void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record); - } - - global interface Writer extends Handler { - Boolean writeOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record); - void writeOnBeforeDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork); - } - - // Add-ons - pick as many as you need - - global interface PriorParentQuery { - Map queryPriorParentsOnBeforeDelete(); - } - - global interface RelatedQuery { - Map queryRelatedOnBeforeDelete(); - } - - global interface RecordsProvider { - List query(TriggerTypes.DeleteRecords records); - String keyOf(SObject row); - } - - global interface OwnUnitOfWork { - DML.Committable ownUnitOfWorkOnBeforeDelete(); - } - - global interface Bypassable { - Boolean bypassOnBeforeDeleteWhen(); - } - - global interface Finalizer { - void finalizeOnBeforeDelete(TriggerTypes.DeleteRecords records); - } - - global interface ContinueOnError { - } - - // Marker - - global interface Handler { - } - - // Implementation - - public class Operation extends TriggerOrchestrator.Operation { - public override List buildExecutors(TriggerOrchestrator orchestrator) { - List executors = new List(); - - for (BeforeDelete.Handler handler : ((TriggerOrchestrator.BeforeDelete) orchestrator.orchestrator).beforeDeleteHandlers()) { - if (handler instanceof BeforeDelete.Validator) { - executors.add(new BeforeDelete.ValidatorExecutor((BeforeDelete.Validator) handler, orchestrator)); - } else if (handler instanceof BeforeDelete.Writer) { - executors.add(new BeforeDelete.WriterExecutor((BeforeDelete.Writer) handler, orchestrator)); - } - } - - return executors; - } - } - - public class ValidatorExecutor extends TriggerOrchestrator.HandlerExecutor { - private BeforeDelete.Validator validator; - - public ValidatorExecutor(BeforeDelete.Validator validator, TriggerOrchestrator orchestrator) { - super(validator, orchestrator); - - this.validator = validator; - this.parentDeclaration = new BeforeDelete.ParentDeclaration(this.validator); - this.relatedLoader = new BeforeDelete.RelatedLoader(this.validator); - this.handlerHooks = new BeforeDelete.HandlerHooks(this.validator); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.validate(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - } catch (Exception e) { - this.handleException(e); - } - } - - private void validate(TriggerTypes.TriggerRecord record) { - if (this.validator.addErrorOnBeforeDeleteWhen(record)) { - this.qualifiedRecords.add(record); - - Integer errorsBefore = record.getOldSObject().getErrors().size(); - - this.validator.addErrorOnBeforeDelete(record); - - if (record.getOldSObject().getErrors().size() == errorsBefore) { - throw new TriggerTypes.TriggerLibException( - this.handlerName + ' qualified a record in addErrorOnBeforeDeleteWhen but attached no error in addErrorOnBeforeDelete.' - ); - } - } - } - } - - public class WriterExecutor extends TriggerOrchestrator.HandlerExecutor { - private BeforeDelete.Writer writer; - private TriggerOrchestrator.UnitOfWork unitOfWork; - - public WriterExecutor(BeforeDelete.Writer writer, TriggerOrchestrator orchestrator) { - super(writer, orchestrator); - - this.writer = writer; - this.parentDeclaration = new BeforeDelete.ParentDeclaration(this.writer); - this.relatedLoader = new BeforeDelete.RelatedLoader(this.writer); - this.unitOfWork = new BeforeDelete.UnitOfWorkResolver(this.writer).resolve(this.orchestrator.sharedUnitOfWork); - this.handlerHooks = new BeforeDelete.HandlerHooks(this.writer); - } - - public override void execute(List records) { - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.write(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - this.commitOwnUnitOfWork(); - } catch (Exception e) { - this.handleException(e); - } - } - - private void write(TriggerTypes.TriggerRecord record) { - if (this.writer.writeOnBeforeDeleteWhen(record)) { - this.qualifiedRecords.add(record); - this.writer.writeOnBeforeDelete(record, this.unitOfWork); - } - } - - private void commitOwnUnitOfWork() { - if (this.qualifiedRecords.isEmpty() || this.unitOfWork === this.orchestrator.sharedUnitOfWork) { - return; - } - - this.unitOfWork.commitWork(); - } - } - - public class ParentDeclaration extends TriggerOrchestrator.ParentDeclaration { - private BeforeDelete.Handler handler; - - public ParentDeclaration(BeforeDelete.Handler handler) { - this.handler = handler; - } - - public override Map getOldParentFields() { - if (this.handler instanceof BeforeDelete.PriorParentQuery) { - return ((BeforeDelete.PriorParentQuery) this.handler).queryPriorParentsOnBeforeDelete(); - } - - return new Map(); - } - } - - public class RelatedLoader extends TriggerOrchestrator.RelatedLoader { - private BeforeDelete.Handler handler; - private Map providers; - - public RelatedLoader(BeforeDelete.Handler handler) { - this.handler = handler; - } - - public override TriggerTypes.RelatedRecords queryRelated(String providerName) { - if (!(this.handler instanceof BeforeDelete.RelatedQuery)) { - return null; - } - - if (this.providers == null) { - this.providers = ((BeforeDelete.RelatedQuery) this.handler).queryRelatedOnBeforeDelete() ?? new Map(); - } - - BeforeDelete.RecordsProvider provider = this.providers.get(providerName); - - if (provider == null) { - return null; - } - - TriggerTypes.DeleteRecords deleteRecords = new TriggerTypes.DeleteTriggerRecords(this.records); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(provider.query(deleteRecords)); - - for (SObject row : related.getRecords()) { - related.groupUnderKey(provider.keyOf(row), row); - } - - return related; - } - } - - public class UnitOfWorkResolver extends TriggerOrchestrator.UnitOfWorkResolver { - private BeforeDelete.Writer writer; - - public UnitOfWorkResolver(BeforeDelete.Writer writer) { - this.writer = writer; - } - - public override TriggerOrchestrator.UnitOfWork resolve(TriggerOrchestrator.UnitOfWork sharedUnitOfWork) { - if (this.writer instanceof BeforeDelete.OwnUnitOfWork) { - return new TriggerOrchestrator.UnitOfWork(((BeforeDelete.OwnUnitOfWork) this.writer).ownUnitOfWorkOnBeforeDelete()); - } - - if (this.writer instanceof BeforeDelete.ContinueOnError) { - return new TriggerOrchestrator.UnitOfWork(); - } - - return sharedUnitOfWork; - } - } - - public class HandlerHooks extends TriggerOrchestrator.HandlerHooks { - private BeforeDelete.Handler handler; - - public HandlerHooks(BeforeDelete.Handler handler) { - this.handler = handler; - } - - public override Boolean isBypassedByAddOn() { - return (this.handler instanceof BeforeDelete.Bypassable && ((BeforeDelete.Bypassable) this.handler).bypassOnBeforeDeleteWhen()); - } - - public override void finalize(List qualifiedRecords) { - if (qualifiedRecords.isEmpty() || !(this.handler instanceof BeforeDelete.Finalizer)) { - return; - } - - ((BeforeDelete.Finalizer) this.handler).finalizeOnBeforeDelete(new TriggerTypes.DeleteTriggerRecords(qualifiedRecords)); - } - - public override Boolean continuesOnError() { - return this.handler instanceof BeforeDelete.ContinueOnError; - } - } - - public class MockedContext extends TriggerOrchestrator.MockedContext implements TriggerOrchestrator.BeforeDeleteMockable, TriggerOrchestrator.BeforeDelete { - public MockedContext() { - super(System.TriggerOperation.BEFORE_DELETE); - } - - public TriggerOrchestrator.BeforeDeleteMockable with(SObject row) { - return this.with(new List{ row }); - } - - public TriggerOrchestrator.BeforeDeleteMockable with(List rows) { - this.oldRecords.addAll(this.assignIds(rows)); - return this; - } - - public TriggerOrchestrator.BeforeDeleteMockable withParent(SObjectField lookupField, SObject parent) { - return this.withParent(lookupField, new List{ parent }); - } - - public TriggerOrchestrator.BeforeDeleteMockable withParent(SObjectField lookupField, List parents) { - this.addParents(lookupField, parents); - return this; - } - - public List beforeDeleteHandlers() { - if (this.handlerOrOrchestrator instanceof TriggerOrchestrator.BeforeDelete) { - return ((TriggerOrchestrator.BeforeDelete) this.handlerOrOrchestrator).beforeDeleteHandlers(); - } - - return new List{ (BeforeDelete.Handler) this.handlerOrOrchestrator }; - } - } -} diff --git a/package/main/default/classes/BeforeDelete.cls-meta.xml b/package/main/default/classes/BeforeDelete.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/BeforeDelete.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/BeforeDeleteTest.cls b/package/main/default/classes/BeforeDeleteTest.cls deleted file mode 100644 index c8d3324..0000000 --- a/package/main/default/classes/BeforeDeleteTest.cls +++ /dev/null @@ -1,482 +0,0 @@ -/** - * @description Tests for the BeforeDelete context of the TriggerOrchestrator class. - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class - * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexUnitTestClassShouldHaveRunAs: The lib does not branch on the running user, so running as one proves nothing - **/ -@SuppressWarnings('PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexUnitTestClassShouldHaveRunAs') -@IsTest(IsParallel=true testFor='ApexClass:BeforeDelete, ApexClass:TriggerOrchestrator') -private class BeforeDeleteTest { - @IsTest - static void handlerWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeDeleteFor(DeletionBlocker.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DeletionBlocker()); - - // Verify - Assert.areEqual('Doe cannot be deleted.', doe.getErrors()[0].getMessage(), 'The handler should block the deletion.'); - } - - @IsTest - static void handlerWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeDeleteFor(DeletionBlockerWithBypassable.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DeletionBlockerWithBypassable()); - - // Verify - Assert.isTrue(doe.getErrors().isEmpty(), 'The handler should not run.'); - } - - @IsTest - static void handler() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeDeleteFor(DeletionBlocker.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DeletionBlocker()); - - // Verify - Assert.areEqual('Doe cannot be deleted.', doe.getErrors()[0].getMessage(), 'The handler should block the deletion.'); - } - - @IsTest - static void handlerWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(LastName = 'Roe'); - - TriggerOrchestrator.mock().beforeDeleteFor(DeletionBlocker.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(new DeletionBlocker()); - - // Verify - Assert.isTrue(roe.getErrors().isEmpty(), 'The handler should not block the deletion.'); - } - - @IsTest - static void validatorWhenNoErrorIsAttached() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeDeleteFor(SilentValidator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new SilentValidator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'SilentValidator qualified a record in addErrorOnBeforeDeleteWhen but attached no error in addErrorOnBeforeDelete.', - expectedException?.getMessage(), - 'The validator should fail when it attaches no error.' - ); - } - - @IsTest - static void writer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - TriggerOrchestrator.mock().beforeDeleteFor(AccountWriter.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriter()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The writer should register the account.'); - } - - @IsTest - static void writerWithOwnUnitOfWork() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('ownUnit').allDmls(); - - TriggerOrchestrator.mock().beforeDeleteFor(AccountWriterWithOwnUnitOfWork.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new AccountWriterWithOwnUnitOfWork()); - - // Verify - Assert.areEqual('Doe', DML.retrieveResultFor('ownUnit').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The own unit of work should commit the account.'); - } - - @IsTest - static void writerWithContinueOnErrorWhenWriteFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - TriggerOrchestrator.mock().beforeDeleteFor(FailingAccountWriterWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingAccountWriterWithContinueOnError()); - - // Verify - Assert.areEqual(0, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The automatic unit of work should not commit.'); - } - - @IsTest - static void handlerWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - DeletionBlockerWithFinalizer blocker = new DeletionBlockerWithFinalizer(); - - TriggerOrchestrator.mock().beforeDeleteFor(DeletionBlockerWithFinalizer.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(blocker); - - // Verify - Assert.areEqual(new Set{ doe.Id }, blocker.finalizedIds, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void handlerWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - DeletionBlockerWithFinalizer blocker = new DeletionBlockerWithFinalizer(); - - TriggerOrchestrator.mock().beforeDeleteFor(DeletionBlockerWithFinalizer.class).with(new Contact(LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(blocker); - - // Verify - Assert.isNull(blocker.finalizedIds, 'The finalizer should not run.'); - } - - @IsTest - static void handlerWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeDeleteFor(FailingDeleteHandler.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDeleteHandler()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The handler failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void handlerWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeDeleteFor(FailingDeleteHandlerWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingDeleteHandlerWithContinueOnError()); - - // Verify - Assert.areEqual('The handler failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void handlerWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeDeleteFor(DmlDeletionBlocker.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DmlDeletionBlocker()); - - // Verify - Assert.areEqual('Doe cannot be deleted.', doe.getErrors()[0].getMessage(), 'The handler should block the deletion.'); - } - - @IsTest - static void priorParentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeDeleteFor(PriorAccountDeletionBlocker.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new PriorAccountDeletionBlocker()); - - // Verify - Assert.areEqual('Contacts of Acme cannot be deleted.', doe.getErrors()[0].getMessage(), 'The handler should block the deletion.'); - } - - @IsTest - static void relatedQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeDeleteFor(RelatedAccountDeletionBlocker.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new RelatedAccountDeletionBlocker()); - - // Verify - Assert.areEqual('Contacts of Acme cannot be deleted.', doe.getErrors()[0].getMessage(), 'The handler should block the deletion.'); - } - - @IsTest - static void mockWith() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeDeleteFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('BEFORE_DELETE', doe.Description, 'The mocked context should run.'); - } - - @IsTest - static void mockWithParent() { - // Setup - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeDeleteFor(PriorAccountDeletionBlocker.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new PriorAccountDeletionBlocker()); - - // Verify - Assert.areEqual('Contacts of Acme cannot be deleted.', doe.getErrors()[0].getMessage(), 'The mocked parent should be served.'); - } - - @IsTest - static void mockWithFakeId() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeDeleteFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.isNotNull(doe.Id, 'The row should get a fake Id.'); - } - - private class OperationStamper implements BeforeDelete.Writer { - public Boolean writeOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnBeforeDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getOldSObject().put(Contact.Description, 'BEFORE_DELETE'); - } - } - - private class DeletionBlockerWithBypassable extends DeletionBlocker implements BeforeDelete.Bypassable { - public Boolean bypassOnBeforeDeleteWhen() { - return true; - } - } - - private class DeletionBlockerWithFinalizer extends DeletionBlocker implements BeforeDelete.Finalizer { - public Set finalizedIds; - - public void finalizeOnBeforeDelete(TriggerTypes.DeleteRecords records) { - this.finalizedIds = records.getIds(); - } - } - - private virtual class FailingDeleteHandler implements BeforeDelete.Writer { - public Boolean writeOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnBeforeDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - throw new TestException('The handler failed.'); - } - } - - private class FailingDeleteHandlerWithContinueOnError extends FailingDeleteHandler implements BeforeDelete.ContinueOnError { - } - - private class DmlDeletionBlocker implements BeforeDelete.Validator { - public Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record) { - Database.setSavepoint(); - record.addError('Doe cannot be deleted.'); - } - } - - private class PriorAccountDeletionBlocker implements BeforeDelete.Validator, BeforeDelete.PriorParentQuery { - public Map queryPriorParentsOnBeforeDelete() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record) { - record.addError('Contacts of ' + (String) record.getOldParent(Contact.AccountId)?.get(Account.Name) + ' cannot be deleted.'); - } - } - - private class RelatedAccountProvider implements BeforeDelete.RecordsProvider { - public List query(TriggerTypes.DeleteRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class RelatedAccountDeletionBlocker implements BeforeDelete.Validator, BeforeDelete.RelatedQuery { - public Map queryRelatedOnBeforeDelete() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record) { - record.addError( - 'Contacts of ' + - (String) record.getRelated('accounts').getFirstWhereKeyEquals(record.getOldSObject().get(Contact.AccountId))?.get(Account.Name) + - ' cannot be deleted.' - ); - } - } - - private virtual class AccountWriterWithOwnUnitOfWork extends AccountWriter implements BeforeDelete.OwnUnitOfWork { - public DML.Committable ownUnitOfWorkOnBeforeDelete() { - return new DML().identifier('ownUnit'); - } - } - - private class FailingAccountWriterWithContinueOnError implements BeforeDelete.Writer, BeforeDelete.ContinueOnError { - public Boolean writeOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnBeforeDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getOldSObject().get(Contact.LastName))); - throw new TestException('The writer failed.'); - } - } - - private class SilentValidator implements BeforeDelete.Validator { - public Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record) { - } - } - - private virtual class AccountWriter implements BeforeDelete.Writer { - public Boolean writeOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnBeforeDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getOldSObject().get(Contact.LastName))); - } - } - - private virtual class DeletionBlocker implements BeforeDelete.Validator { - public Boolean addErrorOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeDelete(TriggerTypes.RejectableDeleteRecord record) { - record.addError('Doe cannot be deleted.'); - } - } - - public class TestLogger implements TriggerOrchestrator.Logger { - public List errors = new List(); - public Integer flushCalls = 0; - - public void log(TriggerOrchestrator.Error error) { - this.errors.add(error); - } - - public void flush() { - this.flushCalls++; - } - } - - private class TestException extends Exception { - } -} diff --git a/package/main/default/classes/BeforeDeleteTest.cls-meta.xml b/package/main/default/classes/BeforeDeleteTest.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/BeforeDeleteTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/BeforeInsert.cls b/package/main/default/classes/BeforeInsert.cls deleted file mode 100644 index 58bdfb4..0000000 --- a/package/main/default/classes/BeforeInsert.cls +++ /dev/null @@ -1,279 +0,0 @@ -global class BeforeInsert { - // Roles - pick one - - global interface Populator extends Handler { - Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record); - void populateOnBeforeInsert(TriggerTypes.InsertRecord record); - } - - global interface Validator extends Handler { - Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record); - void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record); - } - - // Add-ons - pick as many as you need - - global interface ParentQuery { - Map queryParentsOnBeforeInsert(); - } - - global interface RelatedQuery { - Map queryRelatedOnBeforeInsert(); - } - - global interface RecordsProvider { - List query(TriggerTypes.InsertRecords records); - String keyOf(SObject row); - } - - global interface Bypassable { - Boolean bypassOnBeforeInsertWhen(); - } - - global interface Finalizer { - void finalizeOnBeforeInsert(TriggerTypes.InsertRecords records); - } - - global interface ContinueOnError { - } - - // Marker - - global interface Handler { - } - - // Implementation - - public class Operation extends TriggerOrchestrator.Operation { - public override List buildExecutors(TriggerOrchestrator orchestrator) { - List executors = new List(); - - for (BeforeInsert.Handler handler : ((TriggerOrchestrator.BeforeInsert) orchestrator.orchestrator).beforeInsertHandlers()) { - if (handler instanceof BeforeInsert.Populator) { - executors.add(new BeforeInsert.PopulatorExecutor((BeforeInsert.Populator) handler, orchestrator)); - } else if (handler instanceof BeforeInsert.Validator) { - executors.add(new BeforeInsert.ValidatorExecutor((BeforeInsert.Validator) handler, orchestrator)); - } - } - - return executors; - } - } - - public class PopulatorExecutor extends TriggerOrchestrator.HandlerExecutor { - private BeforeInsert.Populator populator; - - public PopulatorExecutor(BeforeInsert.Populator populator, TriggerOrchestrator orchestrator) { - super(populator, orchestrator); - - this.populator = populator; - this.parentDeclaration = new BeforeInsert.ParentDeclaration(this.populator); - this.relatedLoader = new BeforeInsert.RelatedLoader(this.populator); - this.handlerHooks = new BeforeInsert.HandlerHooks(this.populator); - } - - public override void execute(List records) { - Integer dmlStatementsBefore = Limits.getDmlStatements(); - Integer immediateEventsBefore = Limits.getPublishImmediateDML(); - - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.populate(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - } catch (Exception e) { - this.handleException(e); - } - - Integer dmlStatementsAfter = Limits.getDmlStatements(); - Integer immediateEventsAfter = Limits.getPublishImmediateDML(); - - if (dmlStatementsAfter > dmlStatementsBefore || immediateEventsAfter > immediateEventsBefore) { - TriggerTypes.TriggerLibException guardException = new TriggerTypes.TriggerLibException( - this.handlerName + ' performed DML in before insert. Move that work to an after insert Writer.' - ); - - this.logError(guardException); - - throw guardException; - } - - this.orchestrator.parentLoader.refreshNewParents(this.qualifiedRecords); - } - - private void populate(TriggerTypes.TriggerRecord record) { - if (this.populator.populateOnBeforeInsertWhen(record)) { - this.qualifiedRecords.add(record); - this.populator.populateOnBeforeInsert(record); - } - } - } - - public class ValidatorExecutor extends TriggerOrchestrator.HandlerExecutor { - private BeforeInsert.Validator validator; - - public ValidatorExecutor(BeforeInsert.Validator validator, TriggerOrchestrator orchestrator) { - super(validator, orchestrator); - - this.validator = validator; - this.parentDeclaration = new BeforeInsert.ParentDeclaration(this.validator); - this.relatedLoader = new BeforeInsert.RelatedLoader(this.validator); - this.handlerHooks = new BeforeInsert.HandlerHooks(this.validator); - } - - public override void execute(List records) { - Integer dmlStatementsBefore = Limits.getDmlStatements(); - Integer immediateEventsBefore = Limits.getPublishImmediateDML(); - - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.validate(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - } catch (Exception e) { - this.handleException(e); - } - - Integer dmlStatementsAfter = Limits.getDmlStatements(); - Integer immediateEventsAfter = Limits.getPublishImmediateDML(); - - if (dmlStatementsAfter > dmlStatementsBefore || immediateEventsAfter > immediateEventsBefore) { - TriggerTypes.TriggerLibException guardException = new TriggerTypes.TriggerLibException( - this.handlerName + ' performed DML in before insert. Move that work to an after insert Writer.' - ); - - this.logError(guardException); - - throw guardException; - } - } - - private void validate(TriggerTypes.TriggerRecord record) { - if (this.validator.addErrorOnBeforeInsertWhen(record)) { - this.qualifiedRecords.add(record); - - Integer errorsBefore = record.getNewSObject().getErrors().size(); - - this.validator.addErrorOnBeforeInsert(record); - - if (record.getNewSObject().getErrors().size() == errorsBefore) { - throw new TriggerTypes.TriggerLibException( - this.handlerName + ' qualified a record in addErrorOnBeforeInsertWhen but attached no error in addErrorOnBeforeInsert.' - ); - } - } - } - } - - public class ParentDeclaration extends TriggerOrchestrator.ParentDeclaration { - private BeforeInsert.Handler handler; - - public ParentDeclaration(BeforeInsert.Handler handler) { - this.handler = handler; - } - - public override Map getNewParentFields() { - if (this.handler instanceof BeforeInsert.ParentQuery) { - return ((BeforeInsert.ParentQuery) this.handler).queryParentsOnBeforeInsert(); - } - - return new Map(); - } - } - - public class RelatedLoader extends TriggerOrchestrator.RelatedLoader { - private BeforeInsert.Handler handler; - private Map providers; - - public RelatedLoader(BeforeInsert.Handler handler) { - this.handler = handler; - } - - public override TriggerTypes.RelatedRecords queryRelated(String providerName) { - if (!(this.handler instanceof BeforeInsert.RelatedQuery)) { - return null; - } - - if (this.providers == null) { - this.providers = ((BeforeInsert.RelatedQuery) this.handler).queryRelatedOnBeforeInsert() ?? new Map(); - } - - BeforeInsert.RecordsProvider provider = this.providers.get(providerName); - - if (provider == null) { - return null; - } - - TriggerTypes.InsertRecords insertRecords = new TriggerTypes.InsertTriggerRecords(this.records); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(provider.query(insertRecords)); - - for (SObject row : related.getRecords()) { - related.groupUnderKey(provider.keyOf(row), row); - } - - return related; - } - } - - public class HandlerHooks extends TriggerOrchestrator.HandlerHooks { - private BeforeInsert.Handler handler; - - public HandlerHooks(BeforeInsert.Handler handler) { - this.handler = handler; - } - - public override Boolean isBypassedByAddOn() { - return (this.handler instanceof BeforeInsert.Bypassable && ((BeforeInsert.Bypassable) this.handler).bypassOnBeforeInsertWhen()); - } - - public override void finalize(List qualifiedRecords) { - if (qualifiedRecords.isEmpty() || !(this.handler instanceof BeforeInsert.Finalizer)) { - return; - } - - ((BeforeInsert.Finalizer) this.handler).finalizeOnBeforeInsert(new TriggerTypes.InsertTriggerRecords(qualifiedRecords)); - } - - public override Boolean continuesOnError() { - return this.handler instanceof BeforeInsert.ContinueOnError; - } - } - - public class MockedContext extends TriggerOrchestrator.MockedContext implements TriggerOrchestrator.BeforeInsertMockable, TriggerOrchestrator.BeforeInsert { - public MockedContext() { - super(System.TriggerOperation.BEFORE_INSERT); - } - - public TriggerOrchestrator.BeforeInsertMockable with(SObject row) { - return this.with(new List{ row }); - } - - public TriggerOrchestrator.BeforeInsertMockable with(List rows) { - this.newRecords.addAll(rows); - return this; - } - - public TriggerOrchestrator.BeforeInsertMockable withParent(SObjectField lookupField, SObject parent) { - return this.withParent(lookupField, new List{ parent }); - } - - public TriggerOrchestrator.BeforeInsertMockable withParent(SObjectField lookupField, List parents) { - this.addParents(lookupField, parents); - return this; - } - - public List beforeInsertHandlers() { - if (this.handlerOrOrchestrator instanceof TriggerOrchestrator.BeforeInsert) { - return ((TriggerOrchestrator.BeforeInsert) this.handlerOrOrchestrator).beforeInsertHandlers(); - } - - return new List{ (BeforeInsert.Handler) this.handlerOrOrchestrator }; - } - } -} diff --git a/package/main/default/classes/BeforeInsert.cls-meta.xml b/package/main/default/classes/BeforeInsert.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/BeforeInsert.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/BeforeInsertTest.cls b/package/main/default/classes/BeforeInsertTest.cls deleted file mode 100644 index 955164d..0000000 --- a/package/main/default/classes/BeforeInsertTest.cls +++ /dev/null @@ -1,1373 +0,0 @@ -/** - * @description Tests for the BeforeInsert context of the TriggerOrchestrator class. - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class - * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexUnitTestClassShouldHaveRunAs: The lib does not branch on the running user, so running as one proves nothing - **/ -@SuppressWarnings('PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexUnitTestClassShouldHaveRunAs') -@IsTest(IsParallel=true testFor='ApexClass:BeforeInsert, ApexClass:TriggerOrchestrator') -private class BeforeInsertTest { - @IsTest - static void populatorWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void validatorWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(DoeValidator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidator()); - - // Verify - Assert.areEqual('Doe is not allowed.', doe.getErrors()[0].getMessage(), 'The validator should reject the record.'); - } - - @IsTest - static void populatorWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithBypassable.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorWithBypassable()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void populatorWithBypassableWhenBypassIsOff() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithBypassableOff.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorWithBypassableOff()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void populatorWithBypassableWhenManyRecords() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TitlePopulatorWithBypassCounter populator = new TitlePopulatorWithBypassCounter(); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithBypassCounter.class).with(new List{ new Contact(LastName = 'Doe'), new Contact(LastName = 'Roe') }); - - // Test - TriggerOrchestrator.runTestFor(populator); - - // Verify - Assert.areEqual(1, populator.bypassChecks, 'The bypass should be checked once per run.'); - } - - @IsTest - static void parentQueryWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new AccountNamePopulatorWithBypassable()); - handlers.beforeInsert.add(new AccountNamePopulatorWithoutParentQuery()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.isNull(doe.Department, 'The parent should not be queried.'); - } - - @IsTest - static void validatorWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(DoeValidatorWithBypassable.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidatorWithBypassable()); - - // Verify - Assert.isTrue(doe.getErrors().isEmpty(), 'The handler should not run.'); - } - - @IsTest - static void populator() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void populatorWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(LastName = 'Roe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(roe.Title, 'The populator should not set the title.'); - } - - @IsTest - static void validator() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(DoeValidator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidator()); - - // Verify - Assert.areEqual('Doe is not allowed.', doe.getErrors()[0].getMessage(), 'The validator should reject the record.'); - } - - @IsTest - static void validatorWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(LastName = 'Roe'); - - TriggerOrchestrator.mock().beforeInsertFor(DoeValidator.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidator()); - - // Verify - Assert.isTrue(roe.getErrors().isEmpty(), 'The validator should not reject the record.'); - } - - @IsTest - static void validatorWithFieldError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(DoeFieldValidator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DoeFieldValidator()); - - // Verify - Assert.areEqual(new List{ 'Email' }, doe.getErrors()[0].getFields(), 'The error should be attached to the field.'); - } - - @IsTest - static void validatorWithRecordError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(DoeValidator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidator()); - - // Verify - Assert.isTrue(doe.getErrors()[0].getFields().isEmpty(), 'The error should not be attached to a field.'); - } - - @IsTest - static void validatorWithTwoErrors() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(DoubleDoeValidator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DoubleDoeValidator()); - - // Verify - Assert.areEqual(2, doe.getErrors().size(), 'Both errors should be attached.'); - } - - @IsTest - static void validatorWhenNoErrorIsAttached() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(SilentValidator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new SilentValidator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'SilentValidator qualified a record in addErrorOnBeforeInsertWhen but attached no error in addErrorOnBeforeInsert.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void populatorWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - TitlePopulatorWithFinalizer populator = new TitlePopulatorWithFinalizer(); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithFinalizer.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(populator); - - // Verify - Assert.areEqual(new Set{ 'Doe' }, populator.finalizedNames, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void validatorWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - DoeValidatorWithFinalizer validator = new DoeValidatorWithFinalizer(); - - TriggerOrchestrator.mock().beforeInsertFor(DoeValidatorWithFinalizer.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(validator); - - // Verify - Assert.areEqual(new Set{ 'Doe' }, validator.finalizedNames, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void populatorWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(LastName = 'Roe'); - - TitlePopulatorWithFinalizer populator = new TitlePopulatorWithFinalizer(); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithFinalizer.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(populator); - - // Verify - Assert.isNull(populator.finalizedNames, 'The finalizer should not run.'); - } - - @IsTest - static void validatorWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(LastName = 'Roe'); - - DoeValidatorWithFinalizer validator = new DoeValidatorWithFinalizer(); - - TriggerOrchestrator.mock().beforeInsertFor(DoeValidatorWithFinalizer.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(validator); - - // Verify - Assert.isNull(validator.finalizedNames, 'The finalizer should not run.'); - } - - @IsTest - static void populatorWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(FailingPopulator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The populator failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void validatorWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(FailingValidator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingValidator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The validator failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void populatorWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(FailingPopulatorWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingPopulatorWithContinueOnError()); - - // Verify - Assert.areEqual('The populator failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void validatorWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(FailingValidatorWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingValidatorWithContinueOnError()); - - // Verify - Assert.areEqual('The validator failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void populatorWithContinueOnErrorWhenFailingRecordIsNotLast() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorFailingOnDoeWithContinueOnError.class).with(new List{ doe, roe }); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorFailingOnDoeWithContinueOnError()); - - // Verify - Assert.isNull(roe.Title, 'The populator should stop at the failing record.'); - } - - @IsTest - static void finalizerWhenHandlerFailsWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(FailingPopulatorWithContinueOnErrorAndFinalizer.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new FailingPopulatorWithContinueOnErrorAndFinalizer()); - - // Verify - Assert.isNull(doe.Description, 'The finalizer should not run.'); - } - - @IsTest - static void populatorWhenPreviousHandlerFailsWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new FailingPopulatorWithContinueOnError()); - handlers.beforeInsert.add(new TitlePopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void populatorWithContinueOnErrorWhenQualificationFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(PopulatorFailingOnQualificationWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new PopulatorFailingOnQualificationWithContinueOnError()); - - // Verify - Assert.areEqual('The qualification failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void populatorWithContinueOnErrorWhenFinalizerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorFailingOnFinalizeWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorFailingOnFinalizeWithContinueOnError()); - - // Verify - Assert.areEqual('The finalizer failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void populatorWithContinueOnErrorWhenRelatedProviderIsNotRegistered() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(UnregisteredProviderReaderWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new UnregisteredProviderReaderWithContinueOnError()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'No related records provider named unknown is declared. Return it from the queryRelatedOn method of the handler first.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void populatorWithContinueOnErrorWhenNestedRunFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(NestedRunPopulatorWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new NestedRunPopulatorWithContinueOnError()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('Called outside of a trigger context.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void populatorWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(DmlPopulator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new DmlPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'DmlPopulator performed DML in before insert. Move that work to an after insert Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void validatorWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(DmlValidator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new DmlValidator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'DmlValidator performed DML in before insert. Move that work to an after insert Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void populatorWithLoggerWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(DmlPopulator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new DmlPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual(expectedException, logger.errors[0].getException(), 'The guard should log the exception it throws.'); - } - - @IsTest - static void validatorWithLoggerWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(DmlValidator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new DmlValidator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual(expectedException, logger.errors[0].getException(), 'The guard should log the exception it throws.'); - } - - @IsTest - static void populatorWhenQualificationPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(DmlQualificationPopulator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new DmlQualificationPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'DmlQualificationPopulator performed DML in before insert. Move that work to an after insert Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void populatorWhenFinalizerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithDmlFinalizer.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new TitlePopulatorWithDmlFinalizer()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'TitlePopulatorWithDmlFinalizer performed DML in before insert. Move that work to an after insert Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void populatorWhenRelatedQueryPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithDmlRelatedQuery.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new TitlePopulatorWithDmlRelatedQuery()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'TitlePopulatorWithDmlRelatedQuery performed DML in before insert. Move that work to an after insert Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void populatorWhenHandlerPerformsDmlAndFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(FailingDmlPopulator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDmlPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The populator failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void populatorWhenDmlIsPerformedBeforeRun() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Database.setSavepoint(); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void populatorWithContinueOnErrorWhenHandlerPerformsDmlAndFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(FailingDmlPopulatorWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDmlPopulatorWithContinueOnError()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'FailingDmlPopulatorWithContinueOnError performed DML in before insert. Move that work to an after insert Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void validatorWithContinueOnErrorWhenHandlerPerformsDmlAndFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(FailingDmlValidatorWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDmlValidatorWithContinueOnError()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'FailingDmlValidatorWithContinueOnError performed DML in before insert. Move that work to an after insert Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void parentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeInsertFor(AccountNamePopulator.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNamePopulator()); - - // Verify - Assert.areEqual('Acme', doe.Department, 'The populator should copy the account name.'); - } - - @IsTest - static void parentQueryWhenLookupIsRepointed() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Account globex = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Globex'); - SOQL.mock(Account.SObjectType).thenReturn(globex); - - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new AccountRepointingPopulator(globex.Id)); - handlers.beforeInsert.add(new AccountNamePopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('Globex', doe.Department, 'The populator should copy the new account name.'); - } - - @IsTest - static void relatedQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeInsertFor(RelatedAccountNamePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new RelatedAccountNamePopulator()); - - // Verify - Assert.areEqual('Acme', doe.Department, 'The populator should copy the related account name.'); - } - - @IsTest - static void mockWith() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('BEFORE_INSERT', doe.Description, 'The mocked context should run.'); - } - - @IsTest - static void mockWithList() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(OperationStamper.class).with(new List{ doe }); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('BEFORE_INSERT', doe.Description, 'The mocked context should run.'); - } - - @IsTest - static void mockWithTwice() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(OperationStamper.class).with(doe).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('BEFORE_INSERT', doe.Description, 'The first row should still run.'); - } - - @IsTest - static void mockWithParent() { - // Setup - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeInsertFor(AccountNamePopulator.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNamePopulator()); - - // Verify - Assert.areEqual('Acme', doe.Department, 'The mocked parent should be served.'); - } - - @IsTest - static void mockWithParentList() { - // Setup - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeInsertFor(AccountNamePopulator.class).with(doe).withParent(Contact.AccountId, new List{ acme }); - - // Test - TriggerOrchestrator.runTestFor(new AccountNamePopulator()); - - // Verify - Assert.areEqual('Acme', doe.Department, 'The mocked parent should be served.'); - } - - @IsTest - static void mockWithoutId() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.isNull(doe.Id, 'The row should keep no Id.'); - } - - @IsTest - static void mockOrchestrator() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitleOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitleOrchestrator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The mocked orchestrator should run.'); - } - - private class OperationStamper implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.getNewSObject().put(Contact.Description, 'BEFORE_INSERT'); - } - } - - private class TitlePopulatorWithBypassable extends TitlePopulator implements BeforeInsert.Bypassable { - public Boolean bypassOnBeforeInsertWhen() { - return true; - } - } - - private class TitlePopulatorWithBypassableOff extends TitlePopulator implements BeforeInsert.Bypassable { - public Boolean bypassOnBeforeInsertWhen() { - return false; - } - } - - private class TitlePopulatorWithBypassCounter extends TitlePopulator implements BeforeInsert.Bypassable { - public Integer bypassChecks = 0; - - public Boolean bypassOnBeforeInsertWhen() { - this.bypassChecks++; - return false; - } - } - - private class DoeValidatorWithBypassable extends DoeValidator implements BeforeInsert.Bypassable { - public Boolean bypassOnBeforeInsertWhen() { - return true; - } - } - - private class AccountNamePopulatorWithBypassable extends AccountNamePopulator implements BeforeInsert.Bypassable { - public Boolean bypassOnBeforeInsertWhen() { - return true; - } - } - - private class AccountNamePopulatorWithoutParentQuery implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, record.getNewParent(Contact.AccountId)?.get(Account.Name)); - } - } - - private class TitlePopulatorWithFinalizer extends TitlePopulator implements BeforeInsert.Finalizer { - public Set finalizedNames; - - public void finalizeOnBeforeInsert(TriggerTypes.InsertRecords records) { - this.finalizedNames = records.getValuesOf(Contact.LastName); - } - } - - private class DoeValidatorWithFinalizer extends DoeValidator implements BeforeInsert.Finalizer { - public Set finalizedNames; - - public void finalizeOnBeforeInsert(TriggerTypes.InsertRecords records) { - this.finalizedNames = records.getValuesOf(Contact.LastName); - } - } - - private virtual class FailingValidator implements BeforeInsert.Validator { - public Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record) { - throw new TestException('The validator failed.'); - } - } - - private class FailingValidatorWithContinueOnError extends FailingValidator implements BeforeInsert.ContinueOnError { - } - - private class TitlePopulatorFailingOnDoeWithContinueOnError implements BeforeInsert.Populator, BeforeInsert.ContinueOnError { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return true; - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - if (record.equals(Contact.LastName, 'Doe')) { - throw new TestException('The populator failed.'); - } - - record.put(Contact.Title, 'CEO'); - } - } - - private class FailingPopulatorWithContinueOnErrorAndFinalizer extends FailingPopulator implements BeforeInsert.ContinueOnError, BeforeInsert.Finalizer { - public void finalizeOnBeforeInsert(TriggerTypes.InsertRecords records) { - for (TriggerTypes.InsertRecord record : records.getRecords()) { - record.put(Contact.Description, 'Finalized'); - } - } - } - - private class PopulatorFailingOnQualificationWithContinueOnError implements BeforeInsert.Populator, BeforeInsert.ContinueOnError { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - throw new TestException('The qualification failed.'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, 'CEO'); - } - } - - private class TitlePopulatorFailingOnFinalizeWithContinueOnError extends TitlePopulator implements BeforeInsert.Finalizer, BeforeInsert.ContinueOnError { - public void finalizeOnBeforeInsert(TriggerTypes.InsertRecords records) { - throw new TestException('The finalizer failed.'); - } - } - - private class UnregisteredProviderReaderWithContinueOnError implements BeforeInsert.Populator, BeforeInsert.ContinueOnError { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.getRelated('unknown'); - } - } - - private class NestedRunPopulatorWithContinueOnError implements BeforeInsert.Populator, BeforeInsert.ContinueOnError { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - new TriggerOrchestrator(new TestOrchestrator()).run(); - } - } - - private class DmlPopulator implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - Database.setSavepoint(); - } - } - - private class DmlValidator implements BeforeInsert.Validator { - public Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record) { - Database.setSavepoint(); - record.addError('Doe is not allowed.'); - } - } - - private class DmlQualificationPopulator implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - Database.setSavepoint(); - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, 'CEO'); - } - } - - private class TitlePopulatorWithDmlFinalizer extends TitlePopulator implements BeforeInsert.Finalizer { - public void finalizeOnBeforeInsert(TriggerTypes.InsertRecords records) { - Database.setSavepoint(); - } - } - - private class TitlePopulatorWithDmlRelatedQuery implements BeforeInsert.Populator, BeforeInsert.RelatedQuery { - public Map queryRelatedOnBeforeInsert() { - Database.setSavepoint(); - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, record.getRelated('accounts').isEmpty() ? 'CEO' : 'CFO'); - } - } - - private virtual class FailingDmlPopulator implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - Database.setSavepoint(); - throw new TestException('The populator failed.'); - } - } - - private class FailingDmlPopulatorWithContinueOnError extends FailingDmlPopulator implements BeforeInsert.ContinueOnError { - } - - private virtual class FailingDmlValidator implements BeforeInsert.Validator { - public Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record) { - Database.setSavepoint(); - throw new TestException('The validator failed.'); - } - } - - private class FailingDmlValidatorWithContinueOnError extends FailingDmlValidator implements BeforeInsert.ContinueOnError { - } - - private class AccountRepointingPopulator implements BeforeInsert.Populator { - private Id accountId; - - public AccountRepointingPopulator(Id accountId) { - this.accountId = accountId; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.AccountId, this.accountId); - } - } - - private class RelatedAccountProvider implements BeforeInsert.RecordsProvider { - public List query(TriggerTypes.InsertRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class RelatedAccountNamePopulator implements BeforeInsert.Populator, BeforeInsert.RelatedQuery { - public Map queryRelatedOnBeforeInsert() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, record.getRelated('accounts').getFirstWhereKeyEquals(record.getNewSObject().get(Contact.AccountId))?.get(Account.Name)); - } - } - - private class TitleOrchestrator implements TriggerOrchestrator.BeforeInsert { - public List beforeInsertHandlers() { - return new List{ new TitlePopulator() }; - } - } - - private class TestOrchestrator implements TriggerOrchestrator.BeforeInsert { - public List beforeInsert = new List(); - - public List beforeInsertHandlers() { - return this.beforeInsert; - } - } - - private virtual class TitlePopulator implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, 'CEO'); - } - } - - private virtual class DoeValidator implements BeforeInsert.Validator { - public Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record) { - record.addError('Doe is not allowed.'); - } - } - - private class DoeFieldValidator implements BeforeInsert.Validator { - public Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record) { - record.addError(Contact.Email, 'Doe is not allowed.'); - } - } - - private class DoubleDoeValidator implements BeforeInsert.Validator { - public Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record) { - record.addError('Doe is not allowed.'); - record.addError(Contact.Email, 'Doe is not allowed.'); - } - } - - private class SilentValidator implements BeforeInsert.Validator { - public Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record) { - } - } - - private virtual class AccountNamePopulator implements BeforeInsert.Populator, BeforeInsert.ParentQuery { - public Map queryParentsOnBeforeInsert() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, record.getNewParent(Contact.AccountId)?.get(Account.Name)); - } - } - - private virtual class FailingPopulator implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - throw new TestException('The populator failed.'); - } - } - - private class FailingPopulatorWithContinueOnError extends FailingPopulator implements BeforeInsert.ContinueOnError { - } - - public class TestLogger implements TriggerOrchestrator.Logger { - public List errors = new List(); - public Integer flushCalls = 0; - - public void log(TriggerOrchestrator.Error error) { - this.errors.add(error); - } - - public void flush() { - this.flushCalls++; - } - } - - private class TestException extends Exception { - } -} diff --git a/package/main/default/classes/BeforeInsertTest.cls-meta.xml b/package/main/default/classes/BeforeInsertTest.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/BeforeInsertTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/BeforeUpdate.cls b/package/main/default/classes/BeforeUpdate.cls deleted file mode 100644 index 3ee011d..0000000 --- a/package/main/default/classes/BeforeUpdate.cls +++ /dev/null @@ -1,310 +0,0 @@ -global class BeforeUpdate { - // Roles - pick one - - global interface Populator extends Handler { - Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record); - void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record); - } - - global interface Validator extends Handler { - Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record); - void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record); - } - - // Add-ons - pick as many as you need - - global interface ParentQuery { - Map queryParentsOnBeforeUpdate(); - } - - global interface PriorParentQuery { - Map queryPriorParentsOnBeforeUpdate(); - } - - global interface RelatedQuery { - Map queryRelatedOnBeforeUpdate(); - } - - global interface RecordsProvider { - List query(TriggerTypes.UpdateRecords records); - String keyOf(SObject row); - } - - global interface Bypassable { - Boolean bypassOnBeforeUpdateWhen(); - } - - global interface RecursionGuard { - Integer maxRunsPerRecordOnBeforeUpdate(); - } - - global interface Finalizer { - void finalizeOnBeforeUpdate(TriggerTypes.UpdateRecords records); - } - - global interface ContinueOnError { - } - - // Marker - - global interface Handler { - } - - // Implementation - - public class Operation extends TriggerOrchestrator.Operation { - public override List buildExecutors(TriggerOrchestrator orchestrator) { - List executors = new List(); - - for (BeforeUpdate.Handler handler : ((TriggerOrchestrator.BeforeUpdate) orchestrator.orchestrator).beforeUpdateHandlers()) { - if (handler instanceof BeforeUpdate.Populator) { - executors.add(new BeforeUpdate.PopulatorExecutor((BeforeUpdate.Populator) handler, orchestrator)); - } else if (handler instanceof BeforeUpdate.Validator) { - executors.add(new BeforeUpdate.ValidatorExecutor((BeforeUpdate.Validator) handler, orchestrator)); - } - } - - return executors; - } - } - - public class PopulatorExecutor extends TriggerOrchestrator.HandlerExecutor { - private BeforeUpdate.Populator populator; - private TriggerOrchestrator.RecursionTracker recursionTracker; - - public PopulatorExecutor(BeforeUpdate.Populator populator, TriggerOrchestrator orchestrator) { - super(populator, orchestrator); - - this.populator = populator; - this.parentDeclaration = new BeforeUpdate.ParentDeclaration(this.populator); - this.relatedLoader = new BeforeUpdate.RelatedLoader(this.populator); - this.handlerHooks = new BeforeUpdate.HandlerHooks(this.populator); - this.recursionTracker = new BeforeUpdate.RecursionTracker(this.populator, this.handlerName); - } - - public override void execute(List records) { - Integer dmlStatementsBefore = Limits.getDmlStatements(); - Integer immediateEventsBefore = Limits.getPublishImmediateDML(); - - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.populate(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - } catch (Exception e) { - this.handleException(e); - } - - Integer dmlStatementsAfter = Limits.getDmlStatements(); - Integer immediateEventsAfter = Limits.getPublishImmediateDML(); - - if (dmlStatementsAfter > dmlStatementsBefore || immediateEventsAfter > immediateEventsBefore) { - TriggerTypes.TriggerLibException guardException = new TriggerTypes.TriggerLibException( - this.handlerName + ' performed DML in before update. Move that work to an after update Writer.' - ); - - this.logError(guardException); - - throw guardException; - } - - this.orchestrator.parentLoader.refreshNewParents(this.qualifiedRecords); - } - - private void populate(TriggerTypes.TriggerRecord record) { - if (!this.recursionTracker.isLimitReached(record) && this.populator.populateOnBeforeUpdateWhen(record)) { - this.recursionTracker.countRun(record); - this.qualifiedRecords.add(record); - this.populator.populateOnBeforeUpdate(record); - } - } - } - - public class ValidatorExecutor extends TriggerOrchestrator.HandlerExecutor { - private BeforeUpdate.Validator validator; - - public ValidatorExecutor(BeforeUpdate.Validator validator, TriggerOrchestrator orchestrator) { - super(validator, orchestrator); - - this.validator = validator; - this.parentDeclaration = new BeforeUpdate.ParentDeclaration(this.validator); - this.relatedLoader = new BeforeUpdate.RelatedLoader(this.validator); - this.handlerHooks = new BeforeUpdate.HandlerHooks(this.validator); - } - - public override void execute(List records) { - Integer dmlStatementsBefore = Limits.getDmlStatements(); - Integer immediateEventsBefore = Limits.getPublishImmediateDML(); - - try { - this.relatedLoader.attachTo(records); - - for (TriggerTypes.TriggerRecord record : records) { - this.validate(record); - } - - this.handlerHooks.finalize(this.qualifiedRecords); - } catch (Exception e) { - this.handleException(e); - } - - Integer dmlStatementsAfter = Limits.getDmlStatements(); - Integer immediateEventsAfter = Limits.getPublishImmediateDML(); - - if (dmlStatementsAfter > dmlStatementsBefore || immediateEventsAfter > immediateEventsBefore) { - TriggerTypes.TriggerLibException guardException = new TriggerTypes.TriggerLibException( - this.handlerName + ' performed DML in before update. Move that work to an after update Writer.' - ); - - this.logError(guardException); - - throw guardException; - } - } - - private void validate(TriggerTypes.TriggerRecord record) { - if (this.validator.addErrorOnBeforeUpdateWhen(record)) { - this.qualifiedRecords.add(record); - - Integer errorsBefore = record.getNewSObject().getErrors().size(); - - this.validator.addErrorOnBeforeUpdate(record); - - if (record.getNewSObject().getErrors().size() == errorsBefore) { - throw new TriggerTypes.TriggerLibException( - this.handlerName + ' qualified a record in addErrorOnBeforeUpdateWhen but attached no error in addErrorOnBeforeUpdate.' - ); - } - } - } - } - - public class ParentDeclaration extends TriggerOrchestrator.ParentDeclaration { - private BeforeUpdate.Handler handler; - - public ParentDeclaration(BeforeUpdate.Handler handler) { - this.handler = handler; - } - - public override Map getNewParentFields() { - if (this.handler instanceof BeforeUpdate.ParentQuery) { - return ((BeforeUpdate.ParentQuery) this.handler).queryParentsOnBeforeUpdate(); - } - - return new Map(); - } - - public override Map getOldParentFields() { - if (this.handler instanceof BeforeUpdate.PriorParentQuery) { - return ((BeforeUpdate.PriorParentQuery) this.handler).queryPriorParentsOnBeforeUpdate(); - } - - return new Map(); - } - } - - public class RelatedLoader extends TriggerOrchestrator.RelatedLoader { - private BeforeUpdate.Handler handler; - private Map providers; - - public RelatedLoader(BeforeUpdate.Handler handler) { - this.handler = handler; - } - - public override TriggerTypes.RelatedRecords queryRelated(String providerName) { - if (!(this.handler instanceof BeforeUpdate.RelatedQuery)) { - return null; - } - - if (this.providers == null) { - this.providers = ((BeforeUpdate.RelatedQuery) this.handler).queryRelatedOnBeforeUpdate() ?? new Map(); - } - - BeforeUpdate.RecordsProvider provider = this.providers.get(providerName); - - if (provider == null) { - return null; - } - - TriggerTypes.UpdateRecords updateRecords = new TriggerTypes.UpdateTriggerRecords(this.records); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(provider.query(updateRecords)); - - for (SObject row : related.getRecords()) { - related.groupUnderKey(provider.keyOf(row), row); - } - - return related; - } - } - - public class HandlerHooks extends TriggerOrchestrator.HandlerHooks { - private BeforeUpdate.Handler handler; - - public HandlerHooks(BeforeUpdate.Handler handler) { - this.handler = handler; - } - - public override Boolean isBypassedByAddOn() { - return (this.handler instanceof BeforeUpdate.Bypassable && ((BeforeUpdate.Bypassable) this.handler).bypassOnBeforeUpdateWhen()); - } - - public override void finalize(List qualifiedRecords) { - if (qualifiedRecords.isEmpty() || !(this.handler instanceof BeforeUpdate.Finalizer)) { - return; - } - - ((BeforeUpdate.Finalizer) this.handler).finalizeOnBeforeUpdate(new TriggerTypes.UpdateTriggerRecords(qualifiedRecords)); - } - - public override Boolean continuesOnError() { - return this.handler instanceof BeforeUpdate.ContinueOnError; - } - } - - public class RecursionTracker extends TriggerOrchestrator.RecursionTracker { - public RecursionTracker(BeforeUpdate.Handler handler, String handlerName) { - super(handlerName + ':BEFORE_UPDATE'); - - if (handler instanceof BeforeUpdate.RecursionGuard) { - this.maxRunsPerRecord = ((BeforeUpdate.RecursionGuard) handler).maxRunsPerRecordOnBeforeUpdate(); - } - } - } - - public class MockedContext extends TriggerOrchestrator.MockedContext implements TriggerOrchestrator.BeforeUpdateMockable, TriggerOrchestrator.BeforeUpdate { - public MockedContext() { - super(System.TriggerOperation.BEFORE_UPDATE); - } - - public TriggerOrchestrator.BeforeUpdateMockable with(SObject newRow, SObject oldRow) { - return this.with(new List{ newRow }, new List{ oldRow }); - } - - public TriggerOrchestrator.BeforeUpdateMockable with(List newRows, List oldRows) { - this.shareIds(newRows, oldRows); - this.newRecords.addAll(newRows); - this.oldRecords.addAll(oldRows); - return this; - } - - public TriggerOrchestrator.BeforeUpdateMockable withParent(SObjectField lookupField, SObject parent) { - return this.withParent(lookupField, new List{ parent }); - } - - public TriggerOrchestrator.BeforeUpdateMockable withParent(SObjectField lookupField, List parents) { - this.addParents(lookupField, parents); - return this; - } - - public List beforeUpdateHandlers() { - if (this.handlerOrOrchestrator instanceof TriggerOrchestrator.BeforeUpdate) { - return ((TriggerOrchestrator.BeforeUpdate) this.handlerOrOrchestrator).beforeUpdateHandlers(); - } - - return new List{ (BeforeUpdate.Handler) this.handlerOrOrchestrator }; - } - } -} diff --git a/package/main/default/classes/BeforeUpdate.cls-meta.xml b/package/main/default/classes/BeforeUpdate.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/BeforeUpdate.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/BeforeUpdateTest.cls b/package/main/default/classes/BeforeUpdateTest.cls deleted file mode 100644 index 9c8b272..0000000 --- a/package/main/default/classes/BeforeUpdateTest.cls +++ /dev/null @@ -1,1148 +0,0 @@ -/** - * @description Tests for the BeforeUpdate context of the TriggerOrchestrator class. - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class - * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexUnitTestClassShouldHaveRunAs: The lib does not branch on the running user, so running as one proves nothing - **/ -@SuppressWarnings('PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexUnitTestClassShouldHaveRunAs') -@IsTest(IsParallel=true testFor='ApexClass:BeforeUpdate, ApexClass:TriggerOrchestrator') -private class BeforeUpdateTest { - @IsTest - static void populatorWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void validatorWithoutAddOns() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DoeValidator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidator()); - - // Verify - Assert.areEqual('Doe is not allowed.', doe.getErrors()[0].getMessage(), 'The validator should reject the record.'); - } - - @IsTest - static void populatorWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulatorWithBypassable.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorWithBypassable()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void validatorWithBypassable() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DoeValidatorWithBypassable.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidatorWithBypassable()); - - // Verify - Assert.isTrue(doe.getErrors().isEmpty(), 'The handler should not run.'); - } - - @IsTest - static void populator() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void populatorWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulator.class).with(roe, new Contact(Id = roe.Id, LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(roe.Title, 'The populator should not set the title.'); - } - - @IsTest - static void validator() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DoeValidator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidator()); - - // Verify - Assert.areEqual('Doe is not allowed.', doe.getErrors()[0].getMessage(), 'The validator should reject the record.'); - } - - @IsTest - static void validatorWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DoeValidator.class).with(roe, new Contact(Id = roe.Id, LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidator()); - - // Verify - Assert.isTrue(roe.getErrors().isEmpty(), 'The validator should not reject the record.'); - } - - @IsTest - static void validatorWithFieldError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DoeFieldValidator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new DoeFieldValidator()); - - // Verify - Assert.areEqual(new List{ 'Email' }, doe.getErrors()[0].getFields(), 'The error should be attached to the field.'); - } - - @IsTest - static void validatorWithRecordError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DoeValidator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new DoeValidator()); - - // Verify - Assert.isTrue(doe.getErrors()[0].getFields().isEmpty(), 'The error should not be attached to a field.'); - } - - @IsTest - static void validatorWithTwoErrors() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DoubleDoeValidator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new DoubleDoeValidator()); - - // Verify - Assert.areEqual(2, doe.getErrors().size(), 'Both errors should be attached.'); - } - - @IsTest - static void validatorWhenNoErrorIsAttached() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Id doeId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - - TriggerOrchestrator.mock().beforeUpdateFor(SilentValidator.class).with(new Contact(Id = doeId, LastName = 'Doe'), new Contact(Id = doeId, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new SilentValidator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'SilentValidator qualified a record in addErrorOnBeforeUpdateWhen but attached no error in addErrorOnBeforeUpdate.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void populatorWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - TitlePopulatorWithFinalizer populator = new TitlePopulatorWithFinalizer(); - - TriggerOrchestrator.mock() - .beforeUpdateFor(TitlePopulatorWithFinalizer.class) - .with(new List{ doe, roe }, new List{ new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(Id = roe.Id, LastName = 'Roe') }); - - // Test - TriggerOrchestrator.runTestFor(populator); - - // Verify - Assert.areEqual(new Set{ 'Doe' }, populator.finalizedNames, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void validatorWithFinalizer() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - DoeValidatorWithFinalizer validator = new DoeValidatorWithFinalizer(); - - TriggerOrchestrator.mock() - .beforeUpdateFor(DoeValidatorWithFinalizer.class) - .with(new List{ doe, roe }, new List{ new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(Id = roe.Id, LastName = 'Roe') }); - - // Test - TriggerOrchestrator.runTestFor(validator); - - // Verify - Assert.areEqual(new Set{ 'Doe' }, validator.finalizedNames, 'The finalizer should receive the qualified records.'); - } - - @IsTest - static void populatorWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - TitlePopulatorWithFinalizer populator = new TitlePopulatorWithFinalizer(); - - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulatorWithFinalizer.class).with(roe, new Contact(Id = roe.Id, LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(populator); - - // Verify - Assert.isNull(populator.finalizedNames, 'The finalizer should not run.'); - } - - @IsTest - static void validatorWithFinalizerWhenNoRecordQualifies() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact roe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Roe'); - - DoeValidatorWithFinalizer validator = new DoeValidatorWithFinalizer(); - - TriggerOrchestrator.mock().beforeUpdateFor(DoeValidatorWithFinalizer.class).with(roe, new Contact(Id = roe.Id, LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(validator); - - // Verify - Assert.isNull(validator.finalizedNames, 'The finalizer should not run.'); - } - - @IsTest - static void populatorWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(FailingPopulator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The populator failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void validatorWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(FailingValidator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingValidator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('The validator failed.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void populatorWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(FailingPopulatorWithContinueOnError.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingPopulatorWithContinueOnError()); - - // Verify - Assert.areEqual('The populator failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void validatorWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(FailingValidatorWithContinueOnError.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingValidatorWithContinueOnError()); - - // Verify - Assert.areEqual('The validator failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void populatorWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DmlPopulator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new DmlPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'DmlPopulator performed DML in before update. Move that work to an after update Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void validatorWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DmlValidator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new DmlValidator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'DmlValidator performed DML in before update. Move that work to an after update Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void populatorWithLoggerWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DmlPopulator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new DmlPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual(expectedException, logger.errors[0].getException(), 'The guard should log the exception it throws.'); - } - - @IsTest - static void validatorWithLoggerWhenHandlerPerformsDml() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(DmlValidator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new DmlValidator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual(expectedException, logger.errors[0].getException(), 'The guard should log the exception it throws.'); - } - - @IsTest - static void populatorWithDefaultRecursionDepth() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulator.class).with(new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(Id = doe.Id, LastName = 'Doe')); - } - - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.runTestFor(new TitlePopulator()); - } - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The populator should not run beyond the recursion depth.'); - } - - @IsTest - static void populatorWithRecursionGuard() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock() - .beforeUpdateFor(TitlePopulatorWithRecursionGuard.class) - .with(new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(Id = doe.Id, LastName = 'Doe')); - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulatorWithRecursionGuard.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - TriggerOrchestrator.runTestFor(new TitlePopulatorWithRecursionGuard()); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorWithRecursionGuard()); - - // Verify - Assert.isNull(doe.Title, 'The populator should not run beyond its recursion depth.'); - } - - @IsTest - static void populatorQualificationWhenRecursionDepthIsExceeded() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - QualificationCountingPopulator populator = new QualificationCountingPopulator(); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.mock() - .beforeUpdateFor(QualificationCountingPopulator.class) - .with(new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(Id = doe.Id, LastName = 'Doe')); - } - - TriggerOrchestrator.mock().beforeUpdateFor(QualificationCountingPopulator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.runTestFor(populator); - } - - // Test - TriggerOrchestrator.runTestFor(populator); - - // Verify - Assert.areEqual(3, populator.qualificationCalls, 'The qualification should not be asked beyond the recursion depth.'); - } - - @IsTest - static void populatorWithRecursionGuardWhenEarlierRunDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock() - .beforeUpdateFor(TitlePopulatorWithRecursionGuard.class) - .with(new Contact(Id = doe.Id, LastName = 'Roe'), new Contact(Id = doe.Id, LastName = 'Roe')); - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulatorWithRecursionGuard.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - TriggerOrchestrator.runTestFor(new TitlePopulatorWithRecursionGuard()); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorWithRecursionGuard()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void validatorWhenRecursionDepthIsExceeded() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.mock().beforeUpdateFor(DoeValidator.class).with(new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(Id = doe.Id, LastName = 'Doe')); - } - - TriggerOrchestrator.mock().beforeUpdateFor(DoeValidator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - for (Integer run = 0; run < 3; run++) { - TriggerOrchestrator.runTestFor(new DoeValidator()); - } - - // Test - TriggerOrchestrator.runTestFor(new DoeValidator()); - - // Verify - Assert.areEqual('Doe is not allowed.', doe.getErrors()[0].getMessage(), 'The validator should not be limited by the recursion depth.'); - } - - @IsTest - static void populatorWithContinueOnErrorWhenHandlerPerformsDmlAndFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(FailingDmlPopulatorWithContinueOnError.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDmlPopulatorWithContinueOnError()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'FailingDmlPopulatorWithContinueOnError performed DML in before update. Move that work to an after update Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void validatorWithContinueOnErrorWhenHandlerPerformsDmlAndFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(FailingDmlValidatorWithContinueOnError.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingDmlValidatorWithContinueOnError()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'FailingDmlValidatorWithContinueOnError performed DML in before update. Move that work to an after update Writer.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void parentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeUpdateFor(AccountNamePopulator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe')).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNamePopulator()); - - // Verify - Assert.areEqual('Acme', doe.Department, 'The populator should copy the account name.'); - } - - @IsTest - static void priorParentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - TriggerOrchestrator.mock() - .beforeUpdateFor(PriorAccountNamePopulator.class) - .with(doe, new Contact(Id = doe.Id, LastName = 'Doe', AccountId = acme.Id)) - .withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new PriorAccountNamePopulator()); - - // Verify - Assert.areEqual('Acme', doe.Department, 'The populator should copy the prior account name.'); - } - - @IsTest - static void priorParentQueryWithParentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Account globex = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Globex'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock() - .beforeUpdateFor(PriorAccountNamePopulatorWithParentQuery.class) - .with(doe, new Contact(Id = doe.Id, LastName = 'Doe', AccountId = globex.Id)) - .withParent(Contact.AccountId, new List{ acme, globex }); - - // Test - Test.startTest(); - Integer queriesBefore = SOQL.getQueries(); - TriggerOrchestrator.runTestFor(new PriorAccountNamePopulatorWithParentQuery()); - Integer queriesAfter = SOQL.getQueries(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, queriesAfter - queriesBefore, 'The accounts should be queried once.'); - } - - @IsTest - static void parentQueryWhenLookupIsRepointed() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Account globex = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Globex'); - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeUpdate.add(new AccountRepointingPopulator(globex.Id)); - handlers.beforeUpdate.add(new AccountNamePopulator()); - - TriggerOrchestrator.mock() - .beforeUpdateFor(TestOrchestrator.class) - .with(doe, new Contact(Id = doe.Id, LastName = 'Doe', AccountId = acme.Id)) - .withParent(Contact.AccountId, new List{ acme, globex }); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('Globex', doe.Department, 'The populator should copy the new account name.'); - } - - @IsTest - static void relatedQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeUpdateFor(RelatedAccountNamePopulator.class).with(doe, new Contact(Id = doe.Id, LastName = 'Doe', AccountId = acme.Id)); - - // Test - TriggerOrchestrator.runTestFor(new RelatedAccountNamePopulator()); - - // Verify - Assert.areEqual('Acme', doe.Department, 'The populator should copy the related account name.'); - } - - @IsTest - static void mockWithPair() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(OperationStamper.class).with(doe, new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('BEFORE_UPDATE', doe.Description, 'The mocked context should run.'); - } - - @IsTest - static void mockWithPairLists() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(OperationStamper.class).with(new List{ doe }, new List{ new Contact(LastName = 'Doe') }); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('BEFORE_UPDATE', doe.Description, 'The mocked context should run.'); - } - - @IsTest - static void mockWithPairTwice() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock() - .beforeUpdateFor(OperationStamper.class) - .with(doe, new Contact(LastName = 'Doe')) - .with(new Contact(LastName = 'Doe'), new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('BEFORE_UPDATE', doe.Description, 'The first pair should still run.'); - } - - @IsTest - static void mockWithParent() { - // Setup - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeUpdateFor(AccountNamePopulator.class).with(doe, new Contact(LastName = 'Doe')).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNamePopulator()); - - // Verify - Assert.areEqual('Acme', doe.Department, 'The mocked parent should be served.'); - } - - @IsTest - static void mockWithPairSharingOldId() { - // Setup - Id doeId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(OperationStamper.class).with(doe, new Contact(Id = doeId, LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual(doeId, doe.Id, 'The pair should share the old Id.'); - } - - private class OperationStamper implements BeforeUpdate.Populator { - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.getNewSObject().put(Contact.Description, 'BEFORE_UPDATE'); - } - } - - private class TitlePopulatorWithBypassable extends TitlePopulator implements BeforeUpdate.Bypassable { - public Boolean bypassOnBeforeUpdateWhen() { - return true; - } - } - - private class DoeValidatorWithBypassable extends DoeValidator implements BeforeUpdate.Bypassable { - public Boolean bypassOnBeforeUpdateWhen() { - return true; - } - } - - private class TitlePopulatorWithFinalizer extends TitlePopulator implements BeforeUpdate.Finalizer { - public Set finalizedNames; - - public void finalizeOnBeforeUpdate(TriggerTypes.UpdateRecords records) { - this.finalizedNames = records.getValuesOf(Contact.LastName); - } - } - - private class DoeValidatorWithFinalizer extends DoeValidator implements BeforeUpdate.Finalizer { - public Set finalizedNames; - - public void finalizeOnBeforeUpdate(TriggerTypes.UpdateRecords records) { - this.finalizedNames = records.getValuesOf(Contact.LastName); - } - } - - private virtual class FailingValidator implements BeforeUpdate.Validator { - public Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record) { - throw new TestException('The validator failed.'); - } - } - - private class FailingValidatorWithContinueOnError extends FailingValidator implements BeforeUpdate.ContinueOnError { - } - - private class DmlPopulator implements BeforeUpdate.Populator { - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - Database.setSavepoint(); - } - } - - private class DmlValidator implements BeforeUpdate.Validator { - public Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record) { - Database.setSavepoint(); - record.addError('Doe is not allowed.'); - } - } - - private virtual class FailingDmlPopulator implements BeforeUpdate.Populator { - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - Database.setSavepoint(); - throw new TestException('The populator failed.'); - } - } - - private class FailingDmlPopulatorWithContinueOnError extends FailingDmlPopulator implements BeforeUpdate.ContinueOnError { - } - - private virtual class FailingDmlValidator implements BeforeUpdate.Validator { - public Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record) { - Database.setSavepoint(); - throw new TestException('The validator failed.'); - } - } - - private class FailingDmlValidatorWithContinueOnError extends FailingDmlValidator implements BeforeUpdate.ContinueOnError { - } - - private class TitlePopulatorWithRecursionGuard extends TitlePopulator implements BeforeUpdate.RecursionGuard { - public Integer maxRunsPerRecordOnBeforeUpdate() { - return 1; - } - } - - private class QualificationCountingPopulator implements BeforeUpdate.Populator { - public Integer qualificationCalls = 0; - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - this.qualificationCalls++; - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Title, 'CEO'); - } - } - - private virtual class PriorAccountNamePopulator implements BeforeUpdate.Populator, BeforeUpdate.PriorParentQuery { - public Map queryPriorParentsOnBeforeUpdate() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Department, record.getOldParent(Contact.AccountId)?.get(Account.Name)); - } - } - - private class PriorAccountNamePopulatorWithParentQuery extends PriorAccountNamePopulator implements BeforeUpdate.ParentQuery { - public Map queryParentsOnBeforeUpdate() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - } - - private class AccountRepointingPopulator implements BeforeUpdate.Populator { - private Id accountId; - - public AccountRepointingPopulator(Id accountId) { - this.accountId = accountId; - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.AccountId, this.accountId); - } - } - - private class RelatedAccountProvider implements BeforeUpdate.RecordsProvider { - public List query(TriggerTypes.UpdateRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class RelatedAccountNamePopulator implements BeforeUpdate.Populator, BeforeUpdate.RelatedQuery { - public Map queryRelatedOnBeforeUpdate() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Department, record.getRelated('accounts').getFirstWhereKeyEquals(record.getNewSObject().get(Contact.AccountId))?.get(Account.Name)); - } - } - - private class TestOrchestrator implements TriggerOrchestrator.BeforeUpdate { - public List beforeUpdate = new List(); - - public List beforeUpdateHandlers() { - return this.beforeUpdate; - } - } - - private virtual class TitlePopulator implements BeforeUpdate.Populator { - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Title, 'CEO'); - } - } - - private virtual class DoeValidator implements BeforeUpdate.Validator { - public Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record) { - record.addError('Doe is not allowed.'); - } - } - - private class DoeFieldValidator implements BeforeUpdate.Validator { - public Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record) { - record.addError(Contact.Email, 'Doe is not allowed.'); - } - } - - private class DoubleDoeValidator implements BeforeUpdate.Validator { - public Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record) { - record.addError('Doe is not allowed.'); - record.addError(Contact.Email, 'Doe is not allowed.'); - } - } - - private class SilentValidator implements BeforeUpdate.Validator { - public Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record) { - } - } - - private virtual class AccountNamePopulator implements BeforeUpdate.Populator, BeforeUpdate.ParentQuery { - public Map queryParentsOnBeforeUpdate() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Department, record.getNewParent(Contact.AccountId)?.get(Account.Name)); - } - } - - private virtual class FailingPopulator implements BeforeUpdate.Populator { - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - throw new TestException('The populator failed.'); - } - } - - private class FailingPopulatorWithContinueOnError extends FailingPopulator implements BeforeUpdate.ContinueOnError { - } - - public class TestLogger implements TriggerOrchestrator.Logger { - public List errors = new List(); - public Integer flushCalls = 0; - - public void log(TriggerOrchestrator.Error error) { - this.errors.add(error); - } - - public void flush() { - this.flushCalls++; - } - } - - private class TestException extends Exception { - } -} diff --git a/package/main/default/classes/BeforeUpdateTest.cls-meta.xml b/package/main/default/classes/BeforeUpdateTest.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/BeforeUpdateTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/TriggerOrchestrator.cls b/package/main/default/classes/TriggerOrchestrator.cls deleted file mode 100644 index fc6a098..0000000 --- a/package/main/default/classes/TriggerOrchestrator.cls +++ /dev/null @@ -1,1015 +0,0 @@ -/** - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessiveClassLength: It is a library and we tried to put everything into ONE class - * - NcssTypeCount: It is a library and we tried to put everything into ONE class - * - NcssMethodCount: Parent enrichment resolves the new and the old side in one pass to keep it to a single query - * - CyclomaticComplexity: Dispatch has to branch over all seven trigger contexts - * - StdCyclomaticComplexity: Dispatch has to branch over all seven trigger contexts - * - CognitiveComplexity: Dispatch has to branch over all seven trigger contexts - * - AvoidNonRestrictiveQueries: The bypass configuration is custom metadata, it is read once and does not count against SOQL limits - * - FieldDeclarationsShouldBeAtStart: Developer who uses lib should see what's important at start - **/ -@SuppressWarnings( - 'PMD.ExcessiveClassLength,PMD.NcssTypeCount,PMD.NcssMethodCount,PMD.CyclomaticComplexity,PMD.StdCyclomaticComplexity,PMD.CognitiveComplexity,PMD.AvoidNonRestrictiveQueries,PMD.FieldDeclarationsShouldBeAtStart' -) -global with sharing class TriggerOrchestrator { - global interface BeforeInsert { - List beforeInsertHandlers(); - } - - global interface AfterInsert { - List afterInsertHandlers(); - } - - global interface BeforeUpdate { - List beforeUpdateHandlers(); - } - - global interface AfterUpdate { - List afterUpdateHandlers(); - } - - global interface BeforeDelete { - List beforeDeleteHandlers(); - } - - global interface AfterDelete { - List afterDeleteHandlers(); - } - - global interface AfterUndelete { - List afterUndeleteHandlers(); - } - - global interface Logger { - void log(TriggerOrchestrator.Error error); - void flush(); - } - - global interface Error { - String getHandlerName(); - SObjectType getSObjectType(); - System.Exception getException(); - System.TriggerOperation getOperation(); - Set getRecordIds(); - } - - global static void run(Object orchestrator) { - new TriggerOrchestrator(orchestrator).run(); - } - - global static Bypassable bypass() { - return TriggerOrchestrator.triggerBypass; - } - - global interface Bypassable { - Bypassable sObject(SObjectType sObjectType); - Bypassable orchestrator(System.Type orchestrator); - Bypassable handler(System.Type handler); - void all(); - void clear(); - } - - global static Mockable mock() { - return TriggerOrchestrator.triggerMock; - } - - global static void runTestFor(Object handlerOrOrchestrator) { - TriggerOrchestrator.triggerMock.nextContextFor(handlerOrOrchestrator).run(handlerOrOrchestrator); - } - - global interface Mockable { - BeforeInsertMockable beforeInsertFor(System.Type handlerOrOrchestratorType); - AfterInsertMockable afterInsertFor(System.Type handlerOrOrchestratorType); - BeforeUpdateMockable beforeUpdateFor(System.Type handlerOrOrchestratorType); - AfterUpdateMockable afterUpdateFor(System.Type handlerOrOrchestratorType); - BeforeDeleteMockable beforeDeleteFor(System.Type handlerOrOrchestratorType); - AfterDeleteMockable afterDeleteFor(System.Type handlerOrOrchestratorType); - AfterUndeleteMockable afterUndeleteFor(System.Type handlerOrOrchestratorType); - } - - global interface BeforeInsertMockable { - BeforeInsertMockable with(SObject row); - BeforeInsertMockable with(List rows); - BeforeInsertMockable withParent(SObjectField lookupField, SObject parent); - BeforeInsertMockable withParent(SObjectField lookupField, List parents); - } - - global interface AfterInsertMockable { - AfterInsertMockable with(SObject row); - AfterInsertMockable with(List rows); - AfterInsertMockable withParent(SObjectField lookupField, SObject parent); - AfterInsertMockable withParent(SObjectField lookupField, List parents); - } - - global interface BeforeUpdateMockable { - BeforeUpdateMockable with(SObject newRow, SObject oldRow); - BeforeUpdateMockable with(List newRows, List oldRows); - BeforeUpdateMockable withParent(SObjectField lookupField, SObject parent); - BeforeUpdateMockable withParent(SObjectField lookupField, List parents); - } - - global interface AfterUpdateMockable { - AfterUpdateMockable with(SObject newRow, SObject oldRow); - AfterUpdateMockable with(List newRows, List oldRows); - AfterUpdateMockable withParent(SObjectField lookupField, SObject parent); - AfterUpdateMockable withParent(SObjectField lookupField, List parents); - } - - global interface BeforeDeleteMockable { - BeforeDeleteMockable with(SObject row); - BeforeDeleteMockable with(List rows); - BeforeDeleteMockable withParent(SObjectField lookupField, SObject parent); - BeforeDeleteMockable withParent(SObjectField lookupField, List parents); - } - - global interface AfterDeleteMockable { - AfterDeleteMockable with(SObject row); - AfterDeleteMockable with(List rows); - AfterDeleteMockable withParent(SObjectField lookupField, SObject parent); - AfterDeleteMockable withParent(SObjectField lookupField, List parents); - } - - global interface AfterUndeleteMockable { - AfterUndeleteMockable with(SObject row); - AfterUndeleteMockable with(List rows); - AfterUndeleteMockable withParent(SObjectField lookupField, SObject parent); - AfterUndeleteMockable withParent(SObjectField lookupField, List parents); - } - - // Implementations - - @TestVisible - private static TriggerLogger triggerLogger = new TriggerLogger(); - private static MetadataBypass metadataBypass = new MetadataBypass(); - private static TriggerBypass triggerBypass = new TriggerBypass(); - private static TriggerStack triggerStack = new TriggerStack(); - private static RunCounter runCounter = new RunCounter(); - private static TriggerMock triggerMock = new TriggerMock(); - - @TestVisible - private TriggerContext context; - public Object orchestrator; - public ParentLoader parentLoader; - public UnitOfWork sharedUnitOfWork; - - @TestVisible - private TriggerOrchestrator(Object orchestrator) { - this.orchestrator = orchestrator; - this.context = new TriggerContext(); - this.parentLoader = new ParentLoader(this.context); - this.sharedUnitOfWork = new UnitOfWork(); - } - - @TestVisible - private void run() { - Operation operation; - - try { - if (this.context.triggerOperation == null) { - throw new TriggerTypes.TriggerLibException('Called outside of a trigger context.'); - } - - if (TriggerOrchestrator.triggerBypass.isRunBypassed(this)) { - return; - } - - if (TriggerOrchestrator.metadataBypass.isSObjectTriggerBypassed(this.context)) { - return; - } - - if (this.context.triggerOperation == System.TriggerOperation.BEFORE_INSERT && this.orchestrator instanceof TriggerOrchestrator.BeforeInsert) { - operation = new BeforeInsert.Operation(); - } else if (this.context.triggerOperation == System.TriggerOperation.AFTER_INSERT && this.orchestrator instanceof TriggerOrchestrator.AfterInsert) { - operation = new AfterInsert.Operation(); - } else if (this.context.triggerOperation == System.TriggerOperation.BEFORE_UPDATE && this.orchestrator instanceof TriggerOrchestrator.BeforeUpdate) { - operation = new BeforeUpdate.Operation(); - } else if (this.context.triggerOperation == System.TriggerOperation.AFTER_UPDATE && this.orchestrator instanceof TriggerOrchestrator.AfterUpdate) { - operation = new AfterUpdate.Operation(); - } else if (this.context.triggerOperation == System.TriggerOperation.BEFORE_DELETE && this.orchestrator instanceof TriggerOrchestrator.BeforeDelete) { - operation = new BeforeDelete.Operation(); - } else if (this.context.triggerOperation == System.TriggerOperation.AFTER_DELETE && this.orchestrator instanceof TriggerOrchestrator.AfterDelete) { - operation = new AfterDelete.Operation(); - } else if (this.context.triggerOperation == System.TriggerOperation.AFTER_UNDELETE && this.orchestrator instanceof TriggerOrchestrator.AfterUndelete) { - operation = new AfterUndelete.Operation(); - } else { - return; - } - - TriggerOrchestrator.triggerStack.push(operation); - - operation.execute(this); - } finally { - this.finalizeTransactionChunk(operation); - } - } - - private void finalizeTransactionChunk(Operation operation) { - if (operation == null) { - return; - } - - TriggerOrchestrator.triggerStack.popLast(); - - if (TriggerOrchestrator.triggerStack.isMainTransactionChunkCompleted()) { - TriggerOrchestrator.triggerLogger.flush(); - } - } - - private class TriggerContext { - public System.TriggerOperation triggerOperation = Trigger.operationType; - - public List newRecords = Trigger.new; - public List oldRecords = Trigger.old; - - public SObjectType sObjectType { - get { - if (this.sObjectType == null) { - this.sObjectType = (this.newRecords ?? this.oldRecords)?.getSObjectType(); - } - return this.sObjectType; - } - private set; - } - - public List triggerRecords { - get { - if (this.triggerRecords == null) { - this.triggerRecords = TriggerTypes.toTriggerRecords(this.newRecords, this.oldRecords); - } - return this.triggerRecords; - } - private set; - } - } - - private class TriggerBypass implements Bypassable { - private Set bypassed = new Set(); - - public Bypassable sObject(SObjectType sObjectType) { - this.bypassed.add(sObjectType.toString().toLowerCase()); - return this; - } - - public Bypassable orchestrator(System.Type orchestratorType) { - this.bypassed.add(orchestratorType.getName().toLowerCase()); - return this; - } - - public Bypassable handler(System.Type handlerType) { - this.bypassed.add(handlerType.getName().toLowerCase()); - return this; - } - - public void all() { - this.bypassed.add('ALL'); - } - - public void clear() { - this.bypassed.clear(); - } - - public Boolean isRunBypassed(TriggerOrchestrator orchestrator) { - return this.bypassed.contains('ALL') || - this.bypassed.contains(orchestrator.context.sObjectType?.toString()?.toLowerCase()) || - this.bypassed.contains(orchestrator.orchestrator?.toString()?.substringBefore(':')?.toLowerCase()); - } - - public Boolean isHandlerBypassed(String handlerName) { - return this.bypassed.contains('ALL') || this.bypassed.contains(handlerName.toLowerCase()); - } - } - - @TestVisible - private class MetadataBypass { - private Map metadataByObjectName = this.resolveObjectMetadata(); - - public Boolean isSObjectTriggerBypassed(TriggerContext context) { - return this.metadataByObjectName.get(context.sObjectType.toString().toLowerCase())?.objectRow.Bypass__c == true; - } - - public Boolean isHandlerBypassed(TriggerContext context, String handlerName) { - return this.metadataByObjectName.get(context.sObjectType.toString().toLowerCase())?.handlerRowByName.get(handlerName.toLowerCase())?.Bypass__c == true; - } - - private Map resolveObjectMetadata() { - Map metadataByObjectName = new Map(); - String namespacePrefix = String.valueOf(TriggerObject__mdt.SObjectType).removeEnd('TriggerObject__mdt'); - - for ( - TriggerObject__mdt triggerObject : (List) SOQL.of(TriggerObject__mdt.SObjectType) - .with(TriggerObject__mdt.ObjectAPIName__c, TriggerObject__mdt.Bypass__c) - .with(SOQL.SubQuery.of(namespacePrefix + 'TriggerHandlers__r').with(TriggerHandler__mdt.ApexClassName__c, TriggerHandler__mdt.Bypass__c)) - .systemMode() - .mockId('TriggerObject__mdt') - .toList() - ) { - metadataByObjectName.put(triggerObject.ObjectAPIName__c.trim().toLowerCase(), new ObjectMetadata(triggerObject)); - } - - return metadataByObjectName; - } - } - - @TestVisible - private class ObjectMetadata { - public TriggerObject__mdt objectRow = new TriggerObject__mdt(); - public Map handlerRowByName = new Map(); - - public ObjectMetadata(TriggerObject__mdt triggerObject) { - this.objectRow = triggerObject; - this.addHandlerRows(); - } - - private void addHandlerRows() { - for (TriggerHandler__mdt triggerHandler : this.objectRow.TriggerHandlers__r ?? new List()) { - this.addHandlerRow(triggerHandler); - } - } - - private void addHandlerRow(TriggerHandler__mdt triggerHandler) { - String handlerName = triggerHandler.ApexClassName__c.trim().toLowerCase(); - - this.handlerRowByName.put(handlerName, triggerHandler); - - if (handlerName.contains('.')) { - this.handlerRowByName.put(handlerName.substringAfterLast('.'), triggerHandler); - } - } - } - - private class TriggerStack { - private List executionStack = new List(); - - public void push(Operation operation) { - this.executionStack.add(operation); - } - - public void popLast() { - this.executionStack.remove(this.executionStack.size() - 1); - } - - public Boolean isMainTransactionChunkCompleted() { - return this.executionStack.isEmpty(); - } - } - - @TestVisible - private class TriggerLogger { - @TestVisible - private Logger logger = this.resolveLogger(); - - public void log(Error error) { - this.logger?.log(error); - } - - public void flush() { - this.logger?.flush(); - } - - private Logger resolveLogger() { - List implementors = SOQL.of('ApexTypeImplementor') - .with(ApexTypeImplementor.ClassName, ApexTypeImplementor.ClassNamespacePrefix) - .whereAre(SOQL.Filter.with(ApexTypeImplementor.InterfaceName).equal('TriggerOrchestrator.Logger')) - .whereAre(SOQL.Filter.with(ApexTypeImplementor.InterfaceNamespacePrefix).equal(TriggerOrchestrator.class.getName().substringBetween('', '.'))) - .whereAre(SOQL.Filter.with(ApexTypeImplementor.IsConcrete).isTrue()) - .systemMode() - .setLimit(2) - .mockId('ApexTypeImplementor') - .toList(); - - if (implementors.size() > 1) { - throw new TriggerTypes.TriggerLibException('Multiple implementations of TriggerOrchestrator.Logger found. Only one implementation is allowed.'); - } - - if (implementors.isEmpty()) { - return null; - } - - return (Logger) Type.forName(implementors[0].ClassNamespacePrefix, implementors[0].ClassName)?.newInstance(); - } - } - - private class TriggerError implements Error { - public String handlerName; - public SObjectType sObjectType; - public System.Exception thrownException; - public System.TriggerOperation triggerOperation; - public Set recordIds = new Set(); - - public String getHandlerName() { - return this.handlerName; - } - - public SObjectType getSObjectType() { - return this.sObjectType; - } - - public Exception getException() { - return this.thrownException; - } - - public System.TriggerOperation getOperation() { - return this.triggerOperation; - } - - public Set getRecordIds() { - return this.recordIds; - } - - public TriggerError withException(System.Exception thrownException) { - this.thrownException = thrownException; - return this; - } - - public TriggerError withHandlerName(String handlerName) { - this.handlerName = handlerName; - return this; - } - - public TriggerError withContext(TriggerContext context) { - this.sObjectType = context.sObjectType; - this.triggerOperation = context.triggerOperation; - this.setRecordIds(context); - return this; - } - - private void setRecordIds(TriggerContext context) { - for (TriggerTypes.TriggerRecord record : context.triggerRecords) { - if (record.getId() != null) { - this.recordIds.add(record.getId()); - } - } - } - } - - public abstract class Operation { - public void execute(TriggerOrchestrator orchestrator) { - List activeExecutors = this.getActiveExecutors(orchestrator); - - orchestrator.parentLoader.collectDeclarations(activeExecutors); - - for (HandlerExecutor executor : activeExecutors) { - executor.execute(orchestrator.context.triggerRecords); - } - - orchestrator.sharedUnitOfWork.commitWork(); - } - - private List getActiveExecutors(TriggerOrchestrator orchestrator) { - List activeExecutors = new List(); - - for (HandlerExecutor executor : this.buildExecutors(orchestrator)) { - if (!executor.isBypassed()) { - activeExecutors.add(executor); - } - } - - return activeExecutors; - } - - protected abstract List buildExecutors(TriggerOrchestrator orchestrator); - } - - public abstract class HandlerExecutor { - protected String handlerName; - - protected TriggerOrchestrator orchestrator; - protected ParentDeclaration parentDeclaration; - protected RelatedLoader relatedLoader; - protected HandlerHooks handlerHooks; - - protected List qualifiedRecords = new List(); - - public HandlerExecutor(Object handler, TriggerOrchestrator orchestrator) { - this.handlerName = handler.toString().substringBefore(':'); - this.orchestrator = orchestrator; - } - - public Boolean isBypassed() { - return this.isBypassedByApexOrMetadata() || this.handlerHooks.isBypassedByAddOn(); - } - - private Boolean isBypassedByApexOrMetadata() { - return TriggerOrchestrator.triggerBypass.isHandlerBypassed(this.handlerName) || - TriggerOrchestrator.metadataBypass.isHandlerBypassed(this.orchestrator.context, this.handlerName); - } - - public abstract void execute(List records); - - protected void handleException(Exception e) { - this.logError(e); - - if (e instanceof TriggerTypes.TriggerLibException || e instanceof EventBus.RetryableException || !this.handlerHooks.continuesOnError()) { - throw e; - } - } - - protected void logError(Exception e) { - TriggerOrchestrator.triggerLogger.log(new TriggerError().withException(e).withHandlerName(this.handlerName).withContext(this.orchestrator.context)); - } - } - - public abstract class ParentDeclaration { - public virtual Map getNewParentFields() { - return new Map(); - } - - public virtual Map getOldParentFields() { - return new Map(); - } - } - - public class ParentLoader implements TriggerTypes.ParentLoader { - private Set lookupsForNewParents = new Set(); - private Set lookupsForOldParents = new Set(); - private Set loadedLookups = new Set(); - - private Map lookupByRelationshipName = new Map(); - private Map> fieldPathsByLookup = new Map>(); - private Map> cachedParentsByLookup = new Map>(); - - private Boolean areSavedRecordsQueried = false; - - private TriggerContext context; - - public ParentLoader(TriggerContext context) { - this.context = context; - } - - public void collectDeclarations(List handlers) { - for (HandlerExecutor handler : handlers) { - this.declareNewParents(handler.parentDeclaration.getNewParentFields()); - this.declareOldParents(handler.parentDeclaration.getOldParentFields()); - } - - for (TriggerTypes.TriggerRecord record : this.context.triggerRecords) { - record.setParentLoader(this); - } - } - - public void load(String relationshipName) { - SObjectField lookupField = this.lookupByRelationshipName.get(relationshipName); - - if (lookupField == null || this.loadedLookups.contains(lookupField)) { - return; - } - - if (this.lookupsForNewParents.contains(lookupField)) { - this.queryNewParentsFromSavedRecords(); - } - - this.queryAndAttachParents(lookupField); - this.loadedLookups.add(lookupField); - } - - public void refreshNewParents(List records) { - if (records.isEmpty()) { - return; - } - - for (SObjectField lookupField : this.loadedLookups) { - this.queryParents(lookupField, this.notQueriedNewParentIds(lookupField, records)); - this.attachNewParents(lookupField, records); - } - } - - private void declareNewParents(Map parentFieldsByLookup) { - this.lookupsForNewParents.addAll(parentFieldsByLookup.keySet()); - this.declare(parentFieldsByLookup); - } - - private void declareOldParents(Map parentFieldsByLookup) { - this.lookupsForOldParents.addAll(parentFieldsByLookup.keySet()); - this.declare(parentFieldsByLookup); - } - - private void declare(Map parentFieldsByLookup) { - for (SObjectField lookupField : parentFieldsByLookup.keySet()) { - if (!this.fieldPathsByLookup.containsKey(lookupField)) { - this.fieldPathsByLookup.put(lookupField, new Set()); - } - if (!this.cachedParentsByLookup.containsKey(lookupField)) { - this.cachedParentsByLookup.put(lookupField, new Map()); - } - - this.lookupByRelationshipName.put(lookupField.getDescribe().getRelationshipName(), lookupField); - this.fieldPathsByLookup.get(lookupField).addAll(parentFieldsByLookup.get(lookupField).getFields()); - } - } - - private void queryNewParentsFromSavedRecords() { - if (this.areSavedRecordsQueried || !this.haveTriggeredRecordsIds()) { - return; - } - - Set fieldsToQuery = new Set(); - - for (SObjectField lookupField : this.lookupsForNewParents) { - fieldsToQuery.addAll(this.getFieldPathsForLookup(lookupField)); - } - - List savedRecords = SOQL.of(this.context.sObjectType) - .with(fieldsToQuery) - .byIds(this.context.newRecords) - .systemMode() - .withoutSharing() - .mockId('SavedTriggerRecords') - .toList(); - - for (SObjectField lookupField : this.lookupsForNewParents) { - Map cachedParentById = this.cachedParentsByLookup.get(lookupField); - String relationshipName = lookupField.getDescribe().getRelationshipName(); - - for (SObject savedRecord : savedRecords) { - SObject parent = savedRecord.getSObject(relationshipName); - - if (parent != null) { - cachedParentById.put(parent.Id, parent); - } - } - } - - this.areSavedRecordsQueried = true; - } - - private Boolean haveTriggeredRecordsIds() { - return this.context.triggerOperation == System.TriggerOperation.AFTER_INSERT || - this.context.triggerOperation == System.TriggerOperation.AFTER_UPDATE || - this.context.triggerOperation == System.TriggerOperation.AFTER_UNDELETE; - } - - private Set getFieldPathsForLookup(SObjectField lookupField) { - String relationshipName = lookupField.getDescribe().getRelationshipName(); - Set fieldPaths = new Set{ relationshipName + '.Id' }; - - for (String field : this.fieldPathsByLookup.get(lookupField)) { - fieldPaths.add(relationshipName + '.' + field); - } - - return fieldPaths; - } - - private void queryAndAttachParents(SObjectField lookupField) { - Set parentIdsToQuery = new Set(); - - parentIdsToQuery.addAll(this.notQueriedNewParentIds(lookupField, this.context.triggerRecords)); - parentIdsToQuery.addAll(this.notQueriedOldParentIds(lookupField, this.context.triggerRecords)); - - this.queryParents(lookupField, parentIdsToQuery); - - this.attachNewParents(lookupField, this.context.triggerRecords); - this.attachOldParents(lookupField, this.context.triggerRecords); - } - - private Set notQueriedNewParentIds(SObjectField lookupField, List records) { - Set parentIds = new Set(); - - if (!this.lookupsForNewParents.contains(lookupField)) { - return parentIds; - } - - Map cachedParentById = this.cachedParentsByLookup.get(lookupField); - - for (TriggerTypes.TriggerRecord record : records) { - Id parentId = record.getNewParentId(lookupField); - - if (parentId != null && !cachedParentById.containsKey(parentId)) { - parentIds.add(parentId); - } - } - - return parentIds; - } - - private Set notQueriedOldParentIds(SObjectField lookupField, List records) { - Set parentIds = new Set(); - - if (!this.lookupsForOldParents.contains(lookupField)) { - return parentIds; - } - - Map cachedParentById = this.cachedParentsByLookup.get(lookupField); - - for (TriggerTypes.TriggerRecord record : records) { - Id parentId = record.getOldParentId(lookupField); - - if (parentId != null && !cachedParentById.containsKey(parentId)) { - parentIds.add(parentId); - } - } - - return parentIds; - } - - private void queryParents(SObjectField lookupField, Set parentIdsToQuery) { - if (parentIdsToQuery.isEmpty()) { - return; - } - - Map queriedParentById = SOQL.of(lookupField.getDescribe().getReferenceTo()[0]) - .with(this.fieldPathsByLookup.get(lookupField)) - .byIds(parentIdsToQuery) - .systemMode() - .withoutSharing() - .toMap(); - - Map cachedParentById = this.cachedParentsByLookup.get(lookupField); - - for (Id parentId : parentIdsToQuery) { - cachedParentById.put(parentId, queriedParentById.get(parentId)); - } - } - - private void attachNewParents(SObjectField lookupField, List records) { - if (!this.lookupsForNewParents.contains(lookupField)) { - return; - } - - Map parentsById = this.cachedParentsByLookup.get(lookupField); - String relationshipName = lookupField.getDescribe().getRelationshipName(); - - for (TriggerTypes.TriggerRecord record : records) { - record.setNewParent(relationshipName, parentsById.get(record.getNewParentId(lookupField))); - } - } - - private void attachOldParents(SObjectField lookupField, List records) { - if (!this.lookupsForOldParents.contains(lookupField)) { - return; - } - - Map parentsById = this.cachedParentsByLookup.get(lookupField); - String relationshipName = lookupField.getDescribe().getRelationshipName(); - - for (TriggerTypes.TriggerRecord record : records) { - record.setOldParent(relationshipName, parentsById.get(record.getOldParentId(lookupField))); - } - } - } - - public abstract class RelatedLoader implements TriggerTypes.RelatedLoader { - protected List records = new List(); - - private Map relatedByProviderName = new Map(); - - public void attachTo(List records) { - this.records = records; - - for (TriggerTypes.TriggerRecord record : records) { - record.setRelated(this.relatedByProviderName); - record.setRelatedLoader(this); - } - } - - public void load(String providerName) { - if (this.relatedByProviderName.containsKey(providerName)) { - return; - } - - TriggerTypes.RelatedRecords related = this.queryRelated(providerName); - - if (related != null) { - this.relatedByProviderName.put(providerName, related); - } - } - - protected abstract TriggerTypes.RelatedRecords queryRelated(String providerName); - } - - public abstract class UnitOfWorkResolver { - public abstract UnitOfWork resolve(UnitOfWork sharedUnitOfWork); - } - - public class UnitOfWork implements TriggerTypes.UnitOfWork { - private DML.Committable dml; - - public UnitOfWork() { - this(new DML().combineOnDuplicate().systemMode().withoutSharing().identifier('triggerUow')); - } - - public UnitOfWork(DML.Committable dml) { - this.dml = dml; - } - - public TriggerTypes.UnitOfWork toInsert(SObject record) { - this.dml.toInsert(record); - return this; - } - - public TriggerTypes.UnitOfWork toInsert(DML.Record record) { - this.dml.toInsert(record); - return this; - } - - public TriggerTypes.UnitOfWork toUpdate(SObject record) { - this.dml.toUpdate(record); - return this; - } - - public TriggerTypes.UnitOfWork toUpdate(DML.Record record) { - this.dml.toUpdate(record); - return this; - } - - public TriggerTypes.UnitOfWork toUpsert(SObject record, SObjectField externalIdField) { - this.dml.toUpsert(record, externalIdField); - return this; - } - - public TriggerTypes.UnitOfWork toDelete(SObject record) { - this.dml.toDelete(record); - return this; - } - - public TriggerTypes.UnitOfWork toPublish(SObject event) { - this.dml.toPublish(event); - return this; - } - - public void commitWork() { - this.dml.commitWork(); - } - } - - public abstract class HandlerHooks { - public abstract Boolean isBypassedByAddOn(); - public abstract void finalize(List qualifiedRecords); - public abstract Boolean continuesOnError(); - } - - public abstract class RecursionTracker { - private Map runCountByRecordId; - protected Integer maxRunsPerRecord = 3; - - public RecursionTracker(String recursionScope) { - TriggerOrchestrator.runCounter.registerScope(recursionScope); - this.runCountByRecordId = TriggerOrchestrator.runCounter.getForRecursionScope(recursionScope); - } - - public Boolean isLimitReached(TriggerTypes.TriggerRecord record) { - return (this.runCountByRecordId.get(record.getId()) ?? 0) >= this.maxRunsPerRecord; - } - - public void countRun(TriggerTypes.TriggerRecord record) { - this.runCountByRecordId.put(record.getId(), (this.runCountByRecordId.get(record.getId()) ?? 0) + 1); - } - } - - private class RunCounter { - private Map> runCountByRecordIdByScope = new Map>(); - - public void registerScope(String recursionScope) { - if (!this.runCountByRecordIdByScope.containsKey(recursionScope)) { - this.runCountByRecordIdByScope.put(recursionScope, new Map()); - } - } - - public Map getForRecursionScope(String recursionScope) { - return this.runCountByRecordIdByScope.get(recursionScope); - } - } - - private class TriggerMock implements Mockable { - private Map> mockedContextsByClassName = new Map>(); - - public BeforeInsertMockable beforeInsertFor(System.Type handlerOrOrchestratorType) { - BeforeInsert.MockedContext mockedContext = new BeforeInsert.MockedContext(); - this.addMockedContext(handlerOrOrchestratorType, mockedContext); - return mockedContext; - } - - public AfterInsertMockable afterInsertFor(System.Type handlerOrOrchestratorType) { - AfterInsert.MockedContext mockedContext = new AfterInsert.MockedContext(); - this.addMockedContext(handlerOrOrchestratorType, mockedContext); - return mockedContext; - } - - public BeforeUpdateMockable beforeUpdateFor(System.Type handlerOrOrchestratorType) { - BeforeUpdate.MockedContext mockedContext = new BeforeUpdate.MockedContext(); - this.addMockedContext(handlerOrOrchestratorType, mockedContext); - return mockedContext; - } - - public AfterUpdateMockable afterUpdateFor(System.Type handlerOrOrchestratorType) { - AfterUpdate.MockedContext mockedContext = new AfterUpdate.MockedContext(); - this.addMockedContext(handlerOrOrchestratorType, mockedContext); - return mockedContext; - } - - public BeforeDeleteMockable beforeDeleteFor(System.Type handlerOrOrchestratorType) { - BeforeDelete.MockedContext mockedContext = new BeforeDelete.MockedContext(); - this.addMockedContext(handlerOrOrchestratorType, mockedContext); - return mockedContext; - } - - public AfterDeleteMockable afterDeleteFor(System.Type handlerOrOrchestratorType) { - AfterDelete.MockedContext mockedContext = new AfterDelete.MockedContext(); - this.addMockedContext(handlerOrOrchestratorType, mockedContext); - return mockedContext; - } - - public AfterUndeleteMockable afterUndeleteFor(System.Type handlerOrOrchestratorType) { - AfterUndelete.MockedContext mockedContext = new AfterUndelete.MockedContext(); - this.addMockedContext(handlerOrOrchestratorType, mockedContext); - return mockedContext; - } - - public MockedContext nextContextFor(Object handlerOrOrchestrator) { - String className = String.valueOf(handlerOrOrchestrator).substringBefore(':'); - List mockedContexts = this.mockedContextsByClassName.get(className); - - if (mockedContexts == null) { - throw new TriggerTypes.TriggerLibException('No trigger context is mocked for ' + className + '.'); - } - - return mockedContexts.size() > 1 ? mockedContexts.remove(0) : mockedContexts[0]; - } - - private void addMockedContext(System.Type handlerOrOrchestratorType, MockedContext mockedContext) { - String className = ('.' + handlerOrOrchestratorType.getName()).substringAfterLast('.'); - List mockedContexts = this.mockedContextsByClassName.get(className) ?? new List(); - - mockedContexts.add(mockedContext); - this.mockedContextsByClassName.put(className, mockedContexts); - } - } - - public abstract class MockedContext { - protected System.TriggerOperation triggerOperation; - - protected List newRecords = new List(); - protected List oldRecords = new List(); - - protected Map> parentsByType = new Map>(); - - protected Object handlerOrOrchestrator; - - public MockedContext(System.TriggerOperation triggerOperation) { - this.triggerOperation = triggerOperation; - } - - public void run(Object handlerOrOrchestrator) { - this.handlerOrOrchestrator = handlerOrOrchestrator; - - for (SObjectType parentType : this.parentsByType.keySet()) { - SOQL.mock(parentType).thenReturn(this.typed(this.parentsByType.get(parentType))); - } - - SOQL.mock('SavedTriggerRecords').thenReturn(new List()); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(this); - - orchestrator.context.triggerOperation = this.triggerOperation; - orchestrator.context.newRecords = this.typed(this.newRecords); - orchestrator.context.oldRecords = this.typed(this.oldRecords); - - orchestrator.run(); - } - - protected void addParents(SObjectField lookupField, List parents) { - SObjectType parentType = lookupField.getDescribe().getReferenceTo()[0]; - List parentsOfType = this.parentsByType.get(parentType) ?? new List(); - - parentsOfType.addAll(parents); - this.parentsByType.put(parentType, parentsOfType); - } - - protected List assignIds(List records) { - if (records.isEmpty() || !records[0].getSObjectType().getDescribe().fields.getMap().containsKey('Id')) { - return records; - } - - for (SObject record : records) { - record.put('Id', record.Id ?? new TriggerTypes.RandomIdGenerator().get(record.getSObjectType())); - } - - return records; - } - - protected void shareIds(List newRecords, List oldRecords) { - for (Integer i = 0; i < newRecords.size(); i++) { - Id recordId = newRecords[i].Id ?? oldRecords[i].Id ?? new TriggerTypes.RandomIdGenerator().get(newRecords[i].getSObjectType()); - - newRecords[i].put('Id', recordId); - oldRecords[i].put('Id', recordId); - } - } - - private List typed(List records) { - if (records.isEmpty()) { - return null; - } - - List typedRecords = (List) Type.forName('List').newInstance(); - typedRecords.addAll(records); - - return typedRecords; - } - } -} diff --git a/package/main/default/classes/TriggerOrchestrator.cls-meta.xml b/package/main/default/classes/TriggerOrchestrator.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/TriggerOrchestrator.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/TriggerOrchestratorTest.cls b/package/main/default/classes/TriggerOrchestratorTest.cls deleted file mode 100644 index 729d4c2..0000000 --- a/package/main/default/classes/TriggerOrchestratorTest.cls +++ /dev/null @@ -1,2986 +0,0 @@ -/** - * @description Tests for the TriggerOrchestrator class. - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class - * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexUnitTestClassShouldHaveRunAs: The lib does not branch on the running user, so running as one proves nothing - **/ -@SuppressWarnings('PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexUnitTestClassShouldHaveRunAs') -@IsTest(IsParallel=true testFor='ApexClass:TriggerOrchestrator') -private class TriggerOrchestratorTest { - @IsTest - static void runOutsideTriggerContext() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.run(new TestOrchestrator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('Called outside of a trigger context.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void beforeInsertOperation() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - OperationStamper stamper = new OperationStamper(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(stamper); - handlers.afterInsert.add(stamper); - handlers.beforeUpdate.add(stamper); - handlers.afterUpdate.add(stamper); - handlers.beforeDelete.add(stamper); - handlers.afterDelete.add(stamper); - handlers.afterUndelete.add(stamper); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(handlers); - orchestrator.context.triggerOperation = System.TriggerOperation.BEFORE_INSERT; - orchestrator.context.newRecords = new List{ doe }; - - // Test - orchestrator.run(); - - // Verify - Assert.areEqual('BEFORE_INSERT', doe.Description, 'Only the before insert handlers should run.'); - } - - @IsTest - static void afterInsertOperation() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - OperationStamper stamper = new OperationStamper(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(stamper); - handlers.afterInsert.add(stamper); - handlers.beforeUpdate.add(stamper); - handlers.afterUpdate.add(stamper); - handlers.beforeDelete.add(stamper); - handlers.afterDelete.add(stamper); - handlers.afterUndelete.add(stamper); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(handlers); - orchestrator.context.triggerOperation = System.TriggerOperation.AFTER_INSERT; - orchestrator.context.newRecords = new List{ doe }; - - // Test - orchestrator.run(); - - // Verify - Assert.areEqual('AFTER_INSERT', doe.Description, 'Only the after insert handlers should run.'); - } - - @IsTest - static void beforeUpdateOperation() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - OperationStamper stamper = new OperationStamper(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(stamper); - handlers.afterInsert.add(stamper); - handlers.beforeUpdate.add(stamper); - handlers.afterUpdate.add(stamper); - handlers.beforeDelete.add(stamper); - handlers.afterDelete.add(stamper); - handlers.afterUndelete.add(stamper); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(handlers); - orchestrator.context.triggerOperation = System.TriggerOperation.BEFORE_UPDATE; - orchestrator.context.newRecords = new List{ doe }; - orchestrator.context.oldRecords = new List{ new Contact(Id = doe.Id, LastName = 'Doe') }; - - // Test - orchestrator.run(); - - // Verify - Assert.areEqual('BEFORE_UPDATE', doe.Description, 'Only the before update handlers should run.'); - } - - @IsTest - static void afterUpdateOperation() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - OperationStamper stamper = new OperationStamper(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(stamper); - handlers.afterInsert.add(stamper); - handlers.beforeUpdate.add(stamper); - handlers.afterUpdate.add(stamper); - handlers.beforeDelete.add(stamper); - handlers.afterDelete.add(stamper); - handlers.afterUndelete.add(stamper); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(handlers); - orchestrator.context.triggerOperation = System.TriggerOperation.AFTER_UPDATE; - orchestrator.context.newRecords = new List{ doe }; - orchestrator.context.oldRecords = new List{ new Contact(Id = doe.Id, LastName = 'Doe') }; - - // Test - orchestrator.run(); - - // Verify - Assert.areEqual('AFTER_UPDATE', doe.Description, 'Only the after update handlers should run.'); - } - - @IsTest - static void beforeDeleteOperation() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - OperationStamper stamper = new OperationStamper(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(stamper); - handlers.afterInsert.add(stamper); - handlers.beforeUpdate.add(stamper); - handlers.afterUpdate.add(stamper); - handlers.beforeDelete.add(stamper); - handlers.afterDelete.add(stamper); - handlers.afterUndelete.add(stamper); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(handlers); - orchestrator.context.triggerOperation = System.TriggerOperation.BEFORE_DELETE; - orchestrator.context.oldRecords = new List{ doe }; - - // Test - orchestrator.run(); - - // Verify - Assert.areEqual('BEFORE_DELETE', doe.Description, 'Only the before delete handlers should run.'); - } - - @IsTest - static void afterDeleteOperation() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - OperationStamper stamper = new OperationStamper(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(stamper); - handlers.afterInsert.add(stamper); - handlers.beforeUpdate.add(stamper); - handlers.afterUpdate.add(stamper); - handlers.beforeDelete.add(stamper); - handlers.afterDelete.add(stamper); - handlers.afterUndelete.add(stamper); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(handlers); - orchestrator.context.triggerOperation = System.TriggerOperation.AFTER_DELETE; - orchestrator.context.oldRecords = new List{ doe }; - - // Test - orchestrator.run(); - - // Verify - Assert.areEqual('AFTER_DELETE', doe.Description, 'Only the after delete handlers should run.'); - } - - @IsTest - static void afterUndeleteOperation() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - OperationStamper stamper = new OperationStamper(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(stamper); - handlers.afterInsert.add(stamper); - handlers.beforeUpdate.add(stamper); - handlers.afterUpdate.add(stamper); - handlers.beforeDelete.add(stamper); - handlers.afterDelete.add(stamper); - handlers.afterUndelete.add(stamper); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(handlers); - orchestrator.context.triggerOperation = System.TriggerOperation.AFTER_UNDELETE; - orchestrator.context.newRecords = new List{ doe }; - - // Test - orchestrator.run(); - - // Verify - Assert.areEqual('AFTER_UNDELETE', doe.Description, 'Only the after undelete handlers should run.'); - } - - @IsTest - static void nullOrchestrator() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(null); - orchestrator.context.triggerOperation = System.TriggerOperation.BEFORE_INSERT; - orchestrator.context.newRecords = new List{ new Contact(LastName = 'Doe') }; - - Exception expectedException = null; - - // Test - try { - orchestrator.run(); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.isNull(expectedException, 'The run should not throw an exception.'); - } - - @IsTest - static void handlerOrderAcrossRoles() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new DoeValidator()); - handlers.beforeInsert.add(new RejectedRecordPopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('Rejected', doe.Description, 'The validator should run before the populator declared after it.'); - } - - @IsTest - static void handlerOrderAcrossRecords() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - LastNameCollector collector = new LastNameCollector(); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(collector); - handlers.beforeInsert.add(collector); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(new List{ new Contact(LastName = 'Doe'), new Contact(LastName = 'Roe') }); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(new List{ 'Doe', 'Roe', 'Doe', 'Roe' }, collector.lastNames, 'Each handler should process every record before the next handler starts.'); - } - - @IsTest - static void fieldsPopulatedByEarlierHandler() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new TitlePopulator()); - handlers.beforeInsert.add(new CeoDepartmentPopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('Management', doe.Department, 'The later populator should see the title set by the earlier one.'); - } - - @IsTest - static void handlerWithoutRole() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new RolelessHandler()); - handlers.beforeInsert.add(new TitlePopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The handler without a role should be skipped.'); - } - - @IsTest - static void qualifiedRecordsWhenRunTwice() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterInsertFor(IdDispatcher.class).with(doe); - TriggerOrchestrator.mock().afterInsertFor(IdDispatcher.class).with(roe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The second run should not process the records of the first run.'); - } - - @IsTest - static void loggerFlush() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual(1, logger.flushCalls, 'The logger should be flushed once.'); - } - - @IsTest - static void loggerFlushWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(FailingPopulator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual(1, logger.flushCalls, 'The logger should be flushed once.'); - } - - @IsTest - static void loggerFlushWithNestedRun() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(new Contact(LastName = 'Roe')); - TriggerOrchestrator.mock().beforeInsertFor(NestedRunPopulator.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new NestedRunPopulator(new TestOrchestrator())); - - // Verify - Assert.areEqual(1, logger.flushCalls, 'Only the outermost run should flush the logger.'); - } - - @IsTest - static void loggerFlushWhenBypassed() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.bypass().all(); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual(0, logger.flushCalls, 'The logger should not be flushed.'); - } - - @IsTest - static void loggerFromApexTypeImplementor() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor') - .thenReturn( - (ApexTypeImplementor) JSON.deserialize( - '{"attributes":{"type":"ApexTypeImplementor"},"ClassName":"TriggerOrchestratorTest.TestLogger","ClassNamespacePrefix":null}', - ApexTypeImplementor.class - ) - ); - - // Test - TriggerOrchestrator.Logger resolvedLogger = TriggerOrchestrator.triggerLogger.logger; - - // Verify - Assert.isInstanceOfType(resolvedLogger, TestLogger.class, 'The logger should be resolved from ApexTypeImplementor.'); - } - - @IsTest - static void loggerWithMultipleImplementations() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor') - .thenReturn( - (List) JSON.deserialize( - '[{"attributes":{"type":"ApexTypeImplementor"},"ClassName":"FirstLogger","ClassNamespacePrefix":null},{"attributes":{"type":"ApexTypeImplementor"},"ClassName":"SecondLogger","ClassNamespacePrefix":null}]', - List.class - ) - ); - - Exception expectedException = null; - - // Test - try { - new TriggerOrchestrator(new TestOrchestrator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'Multiple implementations of TriggerOrchestrator.Logger found. Only one implementation is allowed.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void configurationQueriesWhenRunTwice() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(new Contact(LastName = 'Doe')); - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(new Contact(LastName = 'Roe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual(2, SOQL.getQueries(), 'The framework should query its configuration once per transaction.'); - } - - @IsTest - static void logWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(FailingPopulatorWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingPopulatorWithContinueOnError()); - - // Verify - Assert.areEqual(1, logger.errors.size(), 'The swallowed failure should be logged.'); - } - - @IsTest - static void logWhenNothingFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isTrue(logger.errors.isEmpty(), 'The logger should log nothing.'); - } - - @IsTest - static void loggedErrorHandlerName() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new DoeValidator()); - handlers.beforeInsert.add(new FailingPopulatorWithContinueOnError()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('FailingPopulatorWithContinueOnError', logger.errors[0].getHandlerName(), 'The logged handler name should match the failing handler.'); - } - - @IsTest - static void loggedErrorSObjectType() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(FailingPopulatorWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingPopulatorWithContinueOnError()); - - // Verify - Assert.areEqual(Contact.SObjectType, logger.errors[0].getSObjectType(), 'The logged SObject type should match the trigger records.'); - } - - @IsTest - static void loggedErrorException() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(FailingPopulator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.isTrue(logger.errors[0].getException() === expectedException, 'The logger should receive the thrown exception.'); - } - - @IsTest - static void loggedErrorOperation() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(FailingPopulatorWithContinueOnError.class).with(doe, new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingPopulatorWithContinueOnError()); - - // Verify - Assert.areEqual(System.TriggerOperation.BEFORE_UPDATE, logger.errors[0].getOperation(), 'The logged operation should match the trigger operation.'); - } - - @IsTest - static void loggedErrorRecordIds() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - Contact doe = new Contact(LastName = 'Doe'); - Contact roe = new Contact(LastName = 'Roe'); - - TriggerOrchestrator.mock() - .beforeUpdateFor(FailingPopulatorWithContinueOnError.class) - .with(new List{ doe, roe }, new List{ new Contact(LastName = 'Doe'), new Contact(LastName = 'Roe') }); - - // Test - TriggerOrchestrator.runTestFor(new FailingPopulatorWithContinueOnError()); - - // Verify - Assert.areEqual(new Set{ doe.Id, roe.Id }, logger.errors[0].getRecordIds(), 'The logged record Ids should include every trigger record.'); - } - - @IsTest - static void loggedErrorRecordIdsOnBeforeInsert() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(FailingPopulatorWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingPopulatorWithContinueOnError()); - - // Verify - Assert.isTrue(logger.errors[0].getRecordIds().isEmpty(), 'The logged record Ids should be empty.'); - } - - @IsTest - static void unimplementedOperation() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(new BeforeInsertOnlyOrchestrator()); - orchestrator.context.triggerOperation = System.TriggerOperation.AFTER_INSERT; - orchestrator.context.newRecords = new List{ new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe') }; - - Exception expectedException = null; - - // Test - try { - orchestrator.run(); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.isNull(expectedException, 'The run should not throw an exception.'); - } - - @IsTest - static void bypassSObject() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.bypass().sObject(Contact.SObjectType); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void bypassSObjectWhenObjectDoesNotMatch() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.bypass().sObject(Account.SObjectType); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void bypassAll() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.bypass().all(); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void bypassClear() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.bypass().sObject(Contact.SObjectType); - TriggerOrchestrator.bypass().clear(); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void bypassOrchestratorWhenTypeDoesNotMatch() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.bypass().orchestrator(TriggerTypes.class); - - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new TitlePopulator()); - - TriggerOrchestrator orchestrator = new TriggerOrchestrator(handlers); - orchestrator.context.triggerOperation = System.TriggerOperation.BEFORE_INSERT; - orchestrator.context.newRecords = new List{ doe }; - - // Test - orchestrator.run(); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void bypassHandlerWhenTypeDoesNotMatch() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.bypass().handler(AccountWriter.class); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void bypassChaining() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - // Test - TriggerOrchestrator.Bypassable chained = TriggerOrchestrator.bypass().sObject(Account.SObjectType).orchestrator(TriggerTypes.class).handler(TriggerTypes.class); - - // Verify - Assert.isTrue(chained === TriggerOrchestrator.bypass(), 'The bypass methods should return the same instance.'); - } - - @IsTest - static void bypassSObjectByMetadata() { - // Setup - TriggerObject__mdt contactSetting = new TriggerObject__mdt(ObjectAPIName__c = 'Contact', Bypass__c = true); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void bypassSObjectByMetadataWhenBypassIsOff() { - // Setup - TriggerObject__mdt contactSetting = new TriggerObject__mdt(ObjectAPIName__c = 'Contact', Bypass__c = false); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void bypassSObjectByMetadataWhenObjectDoesNotMatch() { - // Setup - TriggerObject__mdt accountSetting = new TriggerObject__mdt(ObjectAPIName__c = 'Account', Bypass__c = true); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ accountSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void bypassSObjectByMetadataWithOldRecords() { - // Setup - TriggerObject__mdt contactSetting = new TriggerObject__mdt(ObjectAPIName__c = 'Contact', Bypass__c = true); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - IdDispatcher dispatcher = new IdDispatcher(); - - TriggerOrchestrator.mock().afterDeleteFor(IdDispatcher.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.isTrue(dispatcher.dispatchedIds.isEmpty(), 'The handler should not run.'); - } - - @IsTest - static void bypassSObjectByMetadataWithPaddedName() { - // Setup - TriggerObject__mdt contactSetting = new TriggerObject__mdt(ObjectAPIName__c = ' Contact ', Bypass__c = true); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void bypassSObjectByMetadataWithUpperCaseName() { - // Setup - TriggerObject__mdt contactSetting = new TriggerObject__mdt(ObjectAPIName__c = 'CONTACT', Bypass__c = true); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void bypassHandlerByMetadata() { - // Setup - String namespacePrefix = String.valueOf(TriggerObject__mdt.SObjectType).removeEnd('TriggerObject__mdt'); - TriggerObject__mdt contactSetting = (TriggerObject__mdt) JSON.deserialize( - JSON.serialize( - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerObject__mdt.SObjectType) }, - String.valueOf(TriggerObject__mdt.ObjectAPIName__c) => 'Contact', - String.valueOf(TriggerObject__mdt.Bypass__c) => false, - namespacePrefix + 'TriggerHandlers__r' => new Map{ - 'totalSize' => 1, - 'done' => true, - 'records' => new List{ - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerHandler__mdt.SObjectType) }, - String.valueOf(TriggerHandler__mdt.ApexClassName__c) => 'TitlePopulator', - String.valueOf(TriggerHandler__mdt.Bypass__c) => true - } - } - } - } - ), - TriggerObject__mdt.class - ); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void bypassHandlerByMetadataWithOtherHandlers() { - // Setup - String namespacePrefix = String.valueOf(TriggerObject__mdt.SObjectType).removeEnd('TriggerObject__mdt'); - TriggerObject__mdt contactSetting = (TriggerObject__mdt) JSON.deserialize( - JSON.serialize( - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerObject__mdt.SObjectType) }, - String.valueOf(TriggerObject__mdt.ObjectAPIName__c) => 'Contact', - String.valueOf(TriggerObject__mdt.Bypass__c) => false, - namespacePrefix + 'TriggerHandlers__r' => new Map{ - 'totalSize' => 1, - 'done' => true, - 'records' => new List{ - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerHandler__mdt.SObjectType) }, - String.valueOf(TriggerHandler__mdt.ApexClassName__c) => 'DoeValidator', - String.valueOf(TriggerHandler__mdt.Bypass__c) => true - } - } - } - } - ), - TriggerObject__mdt.class - ); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new DoeValidator()); - handlers.beforeInsert.add(new TitlePopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void bypassHandlerByMetadataWithQualifiedName() { - // Setup - String namespacePrefix = String.valueOf(TriggerObject__mdt.SObjectType).removeEnd('TriggerObject__mdt'); - TriggerObject__mdt contactSetting = (TriggerObject__mdt) JSON.deserialize( - JSON.serialize( - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerObject__mdt.SObjectType) }, - String.valueOf(TriggerObject__mdt.ObjectAPIName__c) => 'Contact', - String.valueOf(TriggerObject__mdt.Bypass__c) => false, - namespacePrefix + 'TriggerHandlers__r' => new Map{ - 'totalSize' => 1, - 'done' => true, - 'records' => new List{ - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerHandler__mdt.SObjectType) }, - String.valueOf(TriggerHandler__mdt.ApexClassName__c) => 'TriggerOrchestratorTest.TitlePopulator', - String.valueOf(TriggerHandler__mdt.Bypass__c) => true - } - } - } - } - ), - TriggerObject__mdt.class - ); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void bypassHandlerByMetadataWhenObjectDoesNotMatch() { - // Setup - String namespacePrefix = String.valueOf(TriggerObject__mdt.SObjectType).removeEnd('TriggerObject__mdt'); - TriggerObject__mdt accountSetting = (TriggerObject__mdt) JSON.deserialize( - JSON.serialize( - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerObject__mdt.SObjectType) }, - String.valueOf(TriggerObject__mdt.ObjectAPIName__c) => 'Account', - String.valueOf(TriggerObject__mdt.Bypass__c) => false, - namespacePrefix + 'TriggerHandlers__r' => new Map{ - 'totalSize' => 1, - 'done' => true, - 'records' => new List{ - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerHandler__mdt.SObjectType) }, - String.valueOf(TriggerHandler__mdt.ApexClassName__c) => 'TitlePopulator', - String.valueOf(TriggerHandler__mdt.Bypass__c) => true - } - } - } - } - ), - TriggerObject__mdt.class - ); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ accountSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should set the title.'); - } - - @IsTest - static void bypassHandlerByMetadataWithBypassable() { - // Setup - String namespacePrefix = String.valueOf(TriggerObject__mdt.SObjectType).removeEnd('TriggerObject__mdt'); - TriggerObject__mdt contactSetting = (TriggerObject__mdt) JSON.deserialize( - JSON.serialize( - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerObject__mdt.SObjectType) }, - String.valueOf(TriggerObject__mdt.ObjectAPIName__c) => 'Contact', - String.valueOf(TriggerObject__mdt.Bypass__c) => false, - namespacePrefix + 'TriggerHandlers__r' => new Map{ - 'totalSize' => 1, - 'done' => true, - 'records' => new List{ - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerHandler__mdt.SObjectType) }, - String.valueOf(TriggerHandler__mdt.ApexClassName__c) => 'TitlePopulatorWithBypassCounter', - String.valueOf(TriggerHandler__mdt.Bypass__c) => true - } - } - } - } - ), - TriggerObject__mdt.class - ); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TitlePopulatorWithBypassCounter populator = new TitlePopulatorWithBypassCounter(); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithBypassCounter.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(populator); - - // Verify - Assert.areEqual(0, populator.bypassChecks, 'The bypass should not be checked.'); - } - - @IsTest - static void bypassHandlerByMetadataWithPaddedName() { - // Setup - String namespacePrefix = String.valueOf(TriggerObject__mdt.SObjectType).removeEnd('TriggerObject__mdt'); - TriggerObject__mdt contactSetting = (TriggerObject__mdt) JSON.deserialize( - JSON.serialize( - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerObject__mdt.SObjectType) }, - String.valueOf(TriggerObject__mdt.ObjectAPIName__c) => 'Contact', - String.valueOf(TriggerObject__mdt.Bypass__c) => false, - namespacePrefix + 'TriggerHandlers__r' => new Map{ - 'totalSize' => 1, - 'done' => true, - 'records' => new List{ - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerHandler__mdt.SObjectType) }, - String.valueOf(TriggerHandler__mdt.ApexClassName__c) => ' TitlePopulator ', - String.valueOf(TriggerHandler__mdt.Bypass__c) => true - } - } - } - } - ), - TriggerObject__mdt.class - ); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void bypassHandlerByMetadataWithUpperCaseName() { - // Setup - String namespacePrefix = String.valueOf(TriggerObject__mdt.SObjectType).removeEnd('TriggerObject__mdt'); - TriggerObject__mdt contactSetting = (TriggerObject__mdt) JSON.deserialize( - JSON.serialize( - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerObject__mdt.SObjectType) }, - String.valueOf(TriggerObject__mdt.ObjectAPIName__c) => 'Contact', - String.valueOf(TriggerObject__mdt.Bypass__c) => false, - namespacePrefix + 'TriggerHandlers__r' => new Map{ - 'totalSize' => 1, - 'done' => true, - 'records' => new List{ - new Map{ - 'attributes' => new Map{ 'type' => String.valueOf(TriggerHandler__mdt.SObjectType) }, - String.valueOf(TriggerHandler__mdt.ApexClassName__c) => 'TITLEPOPULATOR', - String.valueOf(TriggerHandler__mdt.Bypass__c) => true - } - } - } - } - ), - TriggerObject__mdt.class - ); - SOQL.mock('TriggerObject__mdt').thenReturn(new List{ contactSetting }); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulator()); - - // Verify - Assert.isNull(doe.Title, 'The handler should not run.'); - } - - @IsTest - static void finalizerAfterLastRecord() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact janeDoe = new Contact(FirstName = 'Jane', LastName = 'Doe'); - Contact johnDoe = new Contact(FirstName = 'John', LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(TitleCopyingFinalizer.class).with(new List{ janeDoe, johnDoe }); - - // Test - TriggerOrchestrator.runTestFor(new TitleCopyingFinalizer()); - - // Verify - Assert.areEqual('CEO', johnDoe.Description, 'The finalizer should run after the last record is processed.'); - } - - @IsTest - static void finalizerBeforeNextHandler() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new TitleCopyingFinalizer()); - handlers.beforeInsert.add(new DescriptionCopyingPopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('CEO', doe.Department, 'The next handler should see the finalized record.'); - } - - @IsTest - static void loggerWhenHandlerFails() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(FailingPopulator.class).with(new Contact(LastName = 'Doe')); - - // Test - try { - TriggerOrchestrator.runTestFor(new FailingPopulator()); - } catch (TestException expected) { - } - - // Verify - Assert.areEqual(1, logger.errors.size(), 'The failure should be logged once.'); - } - - @IsTest - static void recursionDepthPerRecord() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - Contact janeDoe = new Contact(FirstName = 'Jane', LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeUpdateFor(TitlePopulatorWithRecursionGuard.class).with(new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(LastName = 'Doe')); - TriggerOrchestrator.mock() - .beforeUpdateFor(TitlePopulatorWithRecursionGuard.class) - .with(new List{ doe, janeDoe }, new List{ new Contact(LastName = 'Doe'), new Contact(FirstName = 'Jane', LastName = 'Doe') }); - - TriggerOrchestrator.runTestFor(new TitlePopulatorWithRecursionGuard()); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorWithRecursionGuard()); - - // Verify - Assert.areEqual('CEO', janeDoe.Title, 'The recursion depth should be tracked per record.'); - } - - @IsTest - static void recursionDepthPerHandlerClass() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - IdDispatcherWithRecursionGuard dispatcher = new IdDispatcherWithRecursionGuard(); - - TriggerOrchestrator.mock().afterUpdateFor(AccountWriterWithRecursionGuard.class).with(new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(LastName = 'Doe')); - TriggerOrchestrator.mock().afterUpdateFor(IdDispatcherWithRecursionGuard.class).with(doe, new Contact(LastName = 'Doe')); - - TriggerOrchestrator.runTestFor(new AccountWriterWithRecursionGuard()); - - // Test - TriggerOrchestrator.runTestFor(dispatcher); - - // Verify - Assert.areEqual(new List{ doe.Id }, dispatcher.dispatchedIds, 'The recursion depth should be tracked per handler class.'); - } - - @IsTest - static void recursionDepthSharedByHandlerInstances() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterUpdate.add(new AccountWriterWithRecursionGuard()); - handlers.afterUpdate.add(new AccountWriterWithRecursionGuard()); - - TriggerOrchestrator.mock().afterUpdateFor(TestOrchestrator.class).with(doe, new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(1, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The recursion depth should be shared by handler instances.'); - } - - @IsTest - static void recursionDepthPerContext() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Contact doe = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Doe'); - - DepthOneUpdateHandler depthOneHandler = new DepthOneUpdateHandler(); - - TriggerOrchestrator.mock().beforeUpdateFor(DepthOneUpdateHandler.class).with(new Contact(Id = doe.Id, LastName = 'Doe'), new Contact(LastName = 'Doe')); - TriggerOrchestrator.mock().afterUpdateFor(DepthOneUpdateHandler.class).with(doe, new Contact(LastName = 'Doe')); - - TriggerOrchestrator.runTestFor(depthOneHandler); - - // Test - TriggerOrchestrator.runTestFor(depthOneHandler); - - // Verify - Assert.areEqual(new List{ doe.Id }, depthOneHandler.dispatchedIds, 'The recursion depth should be tracked per context.'); - } - - @IsTest - static void parentQueryWithTwoHandlers() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme', Industry = 'Technology'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new AccountNamePopulator()); - handlers.beforeInsert.add(new AccountIndustryPopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(new Contact(LastName = 'Doe', AccountId = acme.Id)); - - // Test - Test.startTest(); - Integer queriesBefore = SOQL.getQueries(); - TriggerOrchestrator.runTestFor(handlers); - Integer queriesAfter = SOQL.getQueries(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, queriesAfter - queriesBefore, 'The accounts should be queried once.'); - } - - @IsTest - static void parentFieldsWithTwoHandlers() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme', Industry = 'Technology'); - - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new AccountNamePopulator()); - handlers.beforeInsert.add(new AccountIndustryPopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(new List{ 'Acme', 'Technology' }, new List{ doe.Department, doe.Description }, 'The populators should read the fields of both handlers.'); - } - - @IsTest - static void parentQueryWhenDeclaredByLaterHandler() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new UndeclaredAccountNamePopulator()); - handlers.beforeInsert.add(new AccountNamePopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('Acme', doe.Description, 'The populator should copy the account name.'); - } - - @IsTest - static void parentQueryWhenLookupIsEmpty() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(AccountNamePopulator.class).with(new Contact(LastName = 'Doe')); - - // Test - Test.startTest(); - Integer queriesBefore = SOQL.getQueries(); - TriggerOrchestrator.runTestFor(new AccountNamePopulator()); - Integer queriesAfter = SOQL.getQueries(); - Test.stopTest(); - - // Verify - Assert.areEqual(0, queriesAfter - queriesBefore, 'The accounts should not be queried.'); - } - - @IsTest - static void parentQueryWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock() - .beforeInsertFor(AccountNamePopulator.class) - .with(new Contact(LastName = 'Roe', AccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType))); - - // Test - Test.startTest(); - Integer queriesBefore = SOQL.getQueries(); - TriggerOrchestrator.runTestFor(new AccountNamePopulator()); - Integer queriesAfter = SOQL.getQueries(); - Test.stopTest(); - - // Verify - Assert.areEqual(0, queriesAfter - queriesBefore, 'The accounts should not be queried.'); - } - - @IsTest - static void parentQueryWhenOtherLookupIsNotRead() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact manager = new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'Manager'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - TriggerOrchestrator.mock().beforeInsertFor(AccountNamePopulatorWithManagerQuery.class).with(new Contact(LastName = 'Doe', AccountId = acme.Id, ReportsToId = manager.Id)); - - // Test - Test.startTest(); - Integer queriesBefore = SOQL.getQueries(); - TriggerOrchestrator.runTestFor(new AccountNamePopulatorWithManagerQuery()); - Integer queriesAfter = SOQL.getQueries(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, queriesAfter - queriesBefore, 'Only the accounts should be queried.'); - } - - @IsTest - static void parentQueryWhenParentIsNotFound() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - - Contact doe = new Contact(LastName = 'Doe', AccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType)); - - TriggerOrchestrator.mock().beforeInsertFor(AccountNamePopulator.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNamePopulator()); - - // Verify - Assert.isNull(doe.Department, 'The populator should not find the account.'); - } - - @IsTest - static void priorParentQueryWithoutParentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeUpdate.add(new PriorAccountNamePopulator()); - handlers.beforeUpdate.add(new UndeclaredAccountNamePopulator()); - - TriggerOrchestrator.mock().beforeUpdateFor(TestOrchestrator.class).with(doe, new Contact(LastName = 'Doe', AccountId = acme.Id)).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.isNull(doe.Description, 'The populator should not find the account.'); - } - - @IsTest - static void parentQueryWhenLookupIsRepointedToQueriedParent() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Account globex = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Globex'); - SOQL.mock(Account.SObjectType).thenReturn(new List{ acme, globex }); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new AccountRepointingPopulator(globex.Id)); - handlers.beforeInsert.add(new AccountNamePopulator()); - - TriggerOrchestrator.mock() - .beforeInsertFor(TestOrchestrator.class) - .with(new List{ new Contact(LastName = 'Doe', AccountId = acme.Id), new Contact(LastName = 'Roe', AccountId = globex.Id) }); - - // Test - Test.startTest(); - Integer queriesBefore = SOQL.getQueries(); - TriggerOrchestrator.runTestFor(handlers); - Integer queriesAfter = SOQL.getQueries(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, queriesAfter - queriesBefore, 'The accounts should be queried once.'); - } - - @IsTest - static void parentQueryWhenLookupIsCleared() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new AccountRepointingPopulator(null)); - handlers.beforeInsert.add(new AccountNamePopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.isNull(doe.Department, 'The populator should not find the account.'); - } - - @IsTest - static void priorParentQueryWhenLookupIsRepointed() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Account globex = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Globex'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - SOQL.mock(Account.SObjectType).thenReturn(globex); - - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeUpdate.add(new AccountRepointingPopulator(globex.Id)); - handlers.beforeUpdate.add(new PriorAccountNamePopulatorWithParentQuery()); - - TriggerOrchestrator.mock().beforeUpdateFor(TestOrchestrator.class).with(doe, new Contact(LastName = 'Doe', AccountId = acme.Id)); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual('Acme', doe.Department, 'The populator should copy the prior account name.'); - } - - @IsTest - static void relatedQueryWhenRecordDoesNotQualify() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - LastNameProvider provider = new LastNameProvider(); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithRelatedQuery.class).with(new List{ new Contact(LastName = 'Doe'), new Contact(LastName = 'Roe') }); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorWithRelatedQuery(provider)); - - // Verify - Assert.areEqual(new Set{ 'Doe', 'Roe' }, provider.lastNames, 'The provider should receive every record.'); - } - - @IsTest - static void relatedQueryWhenProviderIsNotRead() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - LastNameProvider provider = new LastNameProvider(); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithUnreadRelatedQuery.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorWithUnreadRelatedQuery(provider)); - - // Verify - Assert.isNull(provider.lastNames, 'The provider should not query.'); - } - - @IsTest - static void relatedQueryWhenOtherProviderIsNotRead() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - LastNameProvider otherProvider = new LastNameProvider(); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithTwoRelatedQueries.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new TitlePopulatorWithTwoRelatedQueries(new LastNameProvider(), otherProvider)); - - // Verify - Assert.isNull(otherProvider.lastNames, 'The other provider should not query.'); - } - - @IsTest - static void relatedQueryWithNullProviders() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulatorWithNullRelatedQuery.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new TitlePopulatorWithNullRelatedQuery()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'No related records provider named titles is declared. Return it from the queryRelatedOn method of the handler first.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void relatedQueryWhenDeclaredByAnotherHandler() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - SOQL.mock(Account.SObjectType).thenReturn(new List()); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.beforeInsert.add(new RelatedAccountNamePopulator()); - handlers.beforeInsert.add(new UndeclaredRelatedAccountNamePopulator()); - - TriggerOrchestrator.mock().beforeInsertFor(TestOrchestrator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(handlers); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'No related records provider named accounts is declared. Return it from the queryRelatedOn method of the handler first.', - expectedException?.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void relatedQueryWithParentQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - - ParentReadingProvider provider = new ParentReadingProvider(); - - TriggerOrchestrator.mock() - .beforeInsertFor(AccountNamePopulatorWithRelatedQuery.class) - .with(new Contact(LastName = 'Doe', AccountId = acme.Id)) - .withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountNamePopulatorWithRelatedQuery(provider)); - - // Verify - Assert.areEqual(new Set{ 'Acme' }, provider.accountNames, 'The provider should read the account name.'); - } - - @IsTest - static void relatedQueryWithContinueOnError() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - TestLogger logger = new TestLogger(); - TriggerOrchestrator.triggerLogger.logger = logger; - - TriggerOrchestrator.mock().beforeInsertFor(FailingRelatedQueryPopulatorWithContinueOnError.class).with(new Contact(LastName = 'Doe')); - - // Test - TriggerOrchestrator.runTestFor(new FailingRelatedQueryPopulatorWithContinueOnError()); - - // Verify - Assert.areEqual('The related query failed.', logger.errors[0].getException().getMessage(), 'The logged exception message should match the expected one.'); - } - - @IsTest - static void relatedQueryWithManyRecords() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - TriggerOrchestrator.mock() - .beforeInsertFor(RelatedAccountNamePopulator.class) - .with(new List{ new Contact(LastName = 'Doe', AccountId = acme.Id), new Contact(LastName = 'Roe', AccountId = acme.Id) }); - - // Test - Test.startTest(); - Integer queriesBefore = SOQL.getQueries(); - TriggerOrchestrator.runTestFor(new RelatedAccountNamePopulator()); - Integer queriesAfter = SOQL.getQueries(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, queriesAfter - queriesBefore, 'The related accounts should be queried once.'); - } - - @IsTest - static void relatedQueryWhenReadByManyRecords() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - TriggerOrchestrator.mock() - .beforeInsertFor(RelatedAccountNamePopulator.class) - .with(new List{ new Contact(LastName = 'Doe', AccountId = acme.Id), new Contact(LastName = 'Doe', AccountId = acme.Id) }); - - // Test - Test.startTest(); - Integer queriesBefore = SOQL.getQueries(); - TriggerOrchestrator.runTestFor(new RelatedAccountNamePopulator()); - Integer queriesAfter = SOQL.getQueries(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, queriesAfter - queriesBefore, 'The related accounts should be queried once.'); - } - - @IsTest - static void qualificationWithRelatedQuery() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - SOQL.mock(Account.SObjectType).thenReturn(acme); - - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeInsertFor(RelatedAccountTitlePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new RelatedAccountTitlePopulator()); - - // Verify - Assert.areEqual('CEO', doe.Title, 'The populator should qualify by the related account.'); - } - - @IsTest - static void unitOfWorkToInsertWithDmlRecord() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterInsertFor(DmlRecordInsertWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DmlRecordInsertWriter(acme)); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The unit of work should insert the account.'); - } - - @IsTest - static void unitOfWorkToUpdate() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterInsertFor(AccountUpdateWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountUpdateWriter(acme)); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').updatesOf(Account.SObjectType).records()[0].get(Account.Name), 'The unit of work should update the account.'); - } - - @IsTest - static void unitOfWorkToUpdateWithDmlRecord() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterInsertFor(DmlRecordUpdateWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new DmlRecordUpdateWriter(acme)); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').updatesOf(Account.SObjectType).records()[0].get(Account.Name), 'The unit of work should update the account.'); - } - - @IsTest - static void unitOfWorkToUpsert() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterInsertFor(AccountUpsertWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountUpsertWriter(acme)); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').upsertsOf(Account.SObjectType).records()[0].get(Account.Name), 'The unit of work should upsert the account.'); - } - - @IsTest - static void unitOfWorkToDelete() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme'); - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterInsertFor(AccountDeleteWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountDeleteWriter(acme)); - - // Verify - Assert.areEqual('Acme', DML.retrieveResultFor('triggerUow').deletesOf(Account.SObjectType).records()[0].get(Account.Name), 'The unit of work should delete the account.'); - } - - @IsTest - static void unitOfWorkToPublish() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().afterInsertFor(EventPublishWriter.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new EventPublishWriter(new FlowOrchestrationEvent())); - - // Verify - Assert.areEqual(1, DML.retrieveResultFor('triggerUow').eventsOf(FlowOrchestrationEvent.SObjectType).records().size(), 'The unit of work should publish the event.'); - } - - @IsTest - static void sharedUnitOfWorkWithTwoWriters() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterInsert.add(new AccountWriter()); - handlers.afterInsert.add(new AccountWriter()); - - TriggerOrchestrator.mock().afterInsertFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual(2, DML.retrieveResultFor('triggerUow').insertsOf(Account.SObjectType).records().size(), 'The shared unit of work should commit both accounts.'); - } - - @IsTest - static void sharedUnitOfWorkWhenTwoWritersUpdateSameRecord() { - // Setup - SOQL.mock('TriggerObject__mdt').thenReturn(new List()); - SOQL.mock('ApexTypeImplementor').thenReturn(new List()); - DML.mock('triggerUow').allDmls(); - - Id acmeId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Contact doe = new Contact(LastName = 'Doe'); - - TestOrchestrator handlers = new TestOrchestrator(); - handlers.afterInsert.add(new AccountUpdateWriter(new Account(Id = acmeId, Name = 'Acme'))); - handlers.afterInsert.add(new AccountUpdateWriter(new Account(Id = acmeId, Industry = 'Technology'))); - - TriggerOrchestrator.mock().afterInsertFor(TestOrchestrator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(handlers); - - // Verify - Assert.areEqual( - new List{ new Account(Id = acmeId, Name = 'Acme', Industry = 'Technology') }, - DML.retrieveResultFor('triggerUow').updatesOf(Account.SObjectType).records(), - 'The shared unit of work should merge both updates into one record.' - ); - } - - @IsTest - static void mockQueueTakesFirstContext() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(OperationStamper.class).with(doe); - TriggerOrchestrator.mock().afterInsertFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('BEFORE_INSERT', doe.Description, 'The first mocked context should run.'); - } - - @IsTest - static void mockQueueTakesNextContext() { - // Setup - Contact doe = new Contact(LastName = 'Doe'); - - TriggerOrchestrator.mock().beforeInsertFor(OperationStamper.class).with(doe); - TriggerOrchestrator.mock().afterInsertFor(OperationStamper.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new OperationStamper()); - TriggerOrchestrator.runTestFor(new OperationStamper()); - - // Verify - Assert.areEqual('AFTER_INSERT', doe.Description, 'The next mocked context should run.'); - } - - @IsTest - static void mockQueueReplaysLastContext() { - // Setup - Account acme = new Account(Name = 'Acme', NumberOfEmployees = 0); - - TriggerOrchestrator.mock().beforeInsertFor(EmployeeCounter.class).with(acme); - - // Test - TriggerOrchestrator.runTestFor(new EmployeeCounter()); - TriggerOrchestrator.runTestFor(new EmployeeCounter()); - - // Verify - Assert.areEqual(2, acme.NumberOfEmployees, 'The last mocked context should run again.'); - } - - @IsTest - static void mockForOtherClass() { - // Setup - TriggerOrchestrator.mock().beforeInsertFor(TitlePopulator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new OperationStamper()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('No trigger context is mocked for OperationStamper.', expectedException?.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void mockWithParentWithUndeclaredField() { - // Setup - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme', Industry = 'Banking'); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeInsertFor(UndeclaredIndustryPopulator.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new UndeclaredIndustryPopulator()); - - // Verify - Assert.isNull(doe.Description, 'Only the declared parent fields should be served.'); - } - - @IsTest - static void mockWithParentWithNestedField() { - // Setup - Account acme = new Account(Id = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType), Name = 'Acme', Owner = new User(LastName = 'Smith')); - Contact doe = new Contact(LastName = 'Doe', AccountId = acme.Id); - - TriggerOrchestrator.mock().beforeInsertFor(AccountOwnerPopulator.class).with(doe).withParent(Contact.AccountId, acme); - - // Test - TriggerOrchestrator.runTestFor(new AccountOwnerPopulator()); - - // Verify - Assert.areEqual('Smith', doe.Description, 'The nested parent field should be served.'); - } - - @IsTest - static void mockWithoutParent() { - // Setup - Contact doe = new Contact(LastName = 'Doe', AccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType)); - - TriggerOrchestrator.mock().beforeInsertFor(AccountNamePopulator.class).with(doe); - - // Test - TriggerOrchestrator.runTestFor(new AccountNamePopulator()); - - // Verify - Assert.isNull(doe.Department, 'No parent should be served.'); - } - - @IsTest - static void mockBeforeUpdateForUnimplementedContext() { - // Setup - TriggerOrchestrator.mock().beforeUpdateFor(CeoDepartmentPopulator.class).with(new Contact(LastName = 'Doe'), new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new CeoDepartmentPopulator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.isInstanceOfType(expectedException, System.TypeException.class, 'The unimplemented context should fail.'); - } - - @IsTest - static void mockAfterInsertForUnimplementedOrchestratorContext() { - // Setup - TriggerOrchestrator.mock().afterInsertFor(BeforeInsertOnlyOrchestrator.class).with(new Contact(LastName = 'Doe')); - - Exception expectedException = null; - - // Test - try { - TriggerOrchestrator.runTestFor(new BeforeInsertOnlyOrchestrator()); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.isInstanceOfType(expectedException, System.TypeException.class, 'The unimplemented context should fail.'); - } - - private class OperationStamper implements BeforeInsert.Populator, AfterInsert.Writer, BeforeUpdate.Populator, AfterUpdate.Writer, BeforeDelete.Writer, AfterDelete.Writer, AfterUndelete.Writer { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.getNewSObject().put(Contact.Description, 'BEFORE_INSERT'); - } - - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getNewSObject().put(Contact.Description, 'AFTER_INSERT'); - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.getNewSObject().put(Contact.Description, 'BEFORE_UPDATE'); - } - - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getNewSObject().put(Contact.Description, 'AFTER_UPDATE'); - } - - public Boolean writeOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnBeforeDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getOldSObject().put(Contact.Description, 'BEFORE_DELETE'); - } - - public Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getOldSObject().put(Contact.Description, 'AFTER_DELETE'); - } - - public Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - record.getNewSObject().put(Contact.Description, 'AFTER_UNDELETE'); - } - } - - private class RejectedRecordPopulator implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.getNewSObject().hasErrors(); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Description, 'Rejected'); - } - } - - private class LastNameCollector implements BeforeInsert.Populator { - public List lastNames = new List(); - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return true; - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - this.lastNames.add((String) record.getNewSObject().get(Contact.LastName)); - } - } - - private class CeoDepartmentPopulator implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.Title, 'CEO'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, 'Management'); - } - } - - private class RolelessHandler implements BeforeInsert.Handler { - } - - private class NestedRunPopulator implements BeforeInsert.Populator { - private Object nestedOrchestrator; - - public NestedRunPopulator(Object nestedOrchestrator) { - this.nestedOrchestrator = nestedOrchestrator; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - TriggerOrchestrator.runTestFor(this.nestedOrchestrator); - } - } - - private class BeforeInsertOnlyOrchestrator implements TriggerOrchestrator.BeforeInsert { - public List beforeInsertHandlers() { - return new List(); - } - } - - private class TitlePopulatorWithBypassCounter extends TitlePopulator implements BeforeInsert.Bypassable { - public Integer bypassChecks = 0; - - public Boolean bypassOnBeforeInsertWhen() { - this.bypassChecks++; - return false; - } - } - - private class TitleCopyingFinalizer extends TitlePopulator implements BeforeInsert.Finalizer { - public void finalizeOnBeforeInsert(TriggerTypes.InsertRecords records) { - for (TriggerTypes.InsertRecord record : records.getRecords()) { - record.put(Contact.Description, record.getNewSObject().get(Contact.Title)); - } - } - } - - private class DescriptionCopyingPopulator implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, record.getNewSObject().get(Contact.Description)); - } - } - - private class TitlePopulatorWithRecursionGuard extends TitlePopulator implements BeforeUpdate.RecursionGuard { - public Integer maxRunsPerRecordOnBeforeUpdate() { - return 1; - } - } - - private class AccountWriterWithRecursionGuard extends AccountWriter implements AfterUpdate.RecursionGuard { - public Integer maxRunsPerRecordOnAfterUpdate() { - return 1; - } - } - - private class IdDispatcherWithRecursionGuard extends IdDispatcher implements AfterUpdate.RecursionGuard { - public Integer maxRunsPerRecordOnAfterUpdate() { - return 1; - } - } - - private class DepthOneUpdateHandler implements BeforeUpdate.Populator, BeforeUpdate.RecursionGuard, AfterUpdate.Dispatcher, AfterUpdate.RecursionGuard { - public List dispatchedIds = new List(); - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Title, 'CEO'); - } - - public Integer maxRunsPerRecordOnBeforeUpdate() { - return 1; - } - - public Boolean dispatchOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUpdate(TriggerTypes.UpdateRecords records) { - this.dispatchedIds.addAll(records.getIds()); - } - - public Integer maxRunsPerRecordOnAfterUpdate() { - return 1; - } - } - - private virtual class PriorAccountNamePopulator implements BeforeUpdate.Populator, BeforeUpdate.PriorParentQuery { - public Map queryPriorParentsOnBeforeUpdate() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Department, record.getOldParent(Contact.AccountId)?.get(Account.Name)); - } - } - - private class PriorAccountNamePopulatorWithParentQuery extends PriorAccountNamePopulator implements BeforeUpdate.ParentQuery { - public Map queryParentsOnBeforeUpdate() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - } - - private class UndeclaredAccountNamePopulator implements BeforeInsert.Populator, BeforeUpdate.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Description, record.getNewParent(Contact.AccountId)?.get(Account.Name)); - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Description, record.getNewParent(Contact.AccountId)?.get(Account.Name)); - } - } - - private class AccountIndustryPopulator implements BeforeInsert.Populator, BeforeInsert.ParentQuery { - public Map queryParentsOnBeforeInsert() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Industry) }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Description, record.getNewParent(Contact.AccountId)?.get(Account.Industry)); - } - } - - private class AccountNamePopulatorWithManagerQuery implements BeforeInsert.Populator, BeforeInsert.ParentQuery { - public Map queryParentsOnBeforeInsert() { - return new Map{ - Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name), - Contact.ReportsToId => TriggerTypes.ParentFields.with(Contact.LastName) - }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, record.getNewParent(Contact.AccountId)?.get(Account.Name)); - } - } - - private class AccountRepointingPopulator implements BeforeInsert.Populator, BeforeUpdate.Populator { - private Id accountId; - - public AccountRepointingPopulator(Id accountId) { - this.accountId = accountId; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.AccountId, this.accountId); - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.AccountId, this.accountId); - } - } - - private class RelatedAccountProvider implements BeforeInsert.RecordsProvider, AfterInsert.RecordsProvider, BeforeUpdate.RecordsProvider, AfterUpdate.RecordsProvider, BeforeDelete.RecordsProvider, AfterDelete.RecordsProvider, AfterUndelete.RecordsProvider { - public List query(TriggerTypes.InsertRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public List query(TriggerTypes.UpdateRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public List query(TriggerTypes.DeleteRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public List query(TriggerTypes.UndeleteRecords records) { - return SOQL.of(Account.SObjectType).with(Account.Name).byIds(records.getIdsOf(Contact.AccountId)).toList(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class RelatedAccountNamePopulator implements BeforeInsert.Populator, BeforeInsert.RelatedQuery, BeforeUpdate.Populator, BeforeUpdate.RelatedQuery { - public Map queryRelatedOnBeforeInsert() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, record.getRelated('accounts').getFirstWhereKeyEquals(record.getNewSObject().get(Contact.AccountId))?.get(Account.Name)); - } - - public Map queryRelatedOnBeforeUpdate() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Department, record.getRelated('accounts').getFirstWhereKeyEquals(record.getNewSObject().get(Contact.AccountId))?.get(Account.Name)); - } - } - - private class UndeclaredRelatedAccountNamePopulator implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, record.getRelated('accounts').getFirstWhereKeyEquals(record.getNewSObject().get(Contact.AccountId))?.get(Account.Name)); - } - } - - private class RelatedAccountTitlePopulator implements BeforeInsert.Populator, BeforeInsert.RelatedQuery { - public Map queryRelatedOnBeforeInsert() { - return new Map{ 'accounts' => new RelatedAccountProvider() }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.getRelated('accounts').getFirstWhereKeyEquals(record.getNewSObject().get(Contact.AccountId)) != null; - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, 'CEO'); - } - } - - private class LastNameProvider implements BeforeInsert.RecordsProvider { - public Set lastNames; - - public List query(TriggerTypes.InsertRecords records) { - this.lastNames = records.getValuesOf(Contact.LastName); - return new List(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class ParentReadingProvider implements BeforeInsert.RecordsProvider { - public Set accountNames; - - public List query(TriggerTypes.InsertRecords records) { - this.accountNames = records.getValuesOf('Account', Account.Name); - return new List(); - } - - public String keyOf(SObject row) { - return row.Id; - } - } - - private class TitlePopulatorWithRelatedQuery implements BeforeInsert.Populator, BeforeInsert.RelatedQuery { - private BeforeInsert.RecordsProvider provider; - - public TitlePopulatorWithRelatedQuery(BeforeInsert.RecordsProvider provider) { - this.provider = provider; - } - - public Map queryRelatedOnBeforeInsert() { - return new Map{ 'lastNames' => this.provider }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, record.getRelated('lastNames').isEmpty() ? 'CEO' : 'CFO'); - } - } - - private class TitlePopulatorWithUnreadRelatedQuery extends TitlePopulator implements BeforeInsert.RelatedQuery { - private BeforeInsert.RecordsProvider provider; - - public TitlePopulatorWithUnreadRelatedQuery(BeforeInsert.RecordsProvider provider) { - this.provider = provider; - } - - public Map queryRelatedOnBeforeInsert() { - return new Map{ 'lastNames' => this.provider }; - } - } - - private class TitlePopulatorWithTwoRelatedQueries implements BeforeInsert.Populator, BeforeInsert.RelatedQuery { - private BeforeInsert.RecordsProvider provider; - private BeforeInsert.RecordsProvider otherProvider; - - public TitlePopulatorWithTwoRelatedQueries(BeforeInsert.RecordsProvider provider, BeforeInsert.RecordsProvider otherProvider) { - this.provider = provider; - this.otherProvider = otherProvider; - } - - public Map queryRelatedOnBeforeInsert() { - return new Map{ 'lastNames' => this.provider, 'otherLastNames' => this.otherProvider }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, record.getRelated('lastNames').isEmpty() ? 'CEO' : 'CFO'); - } - } - - private class TitlePopulatorWithNullRelatedQuery implements BeforeInsert.Populator, BeforeInsert.RelatedQuery { - public Map queryRelatedOnBeforeInsert() { - return null; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, record.getRelated('titles').isEmpty() ? 'CEO' : 'CFO'); - } - } - - private class AccountNamePopulatorWithRelatedQuery implements BeforeInsert.Populator, BeforeInsert.ParentQuery, BeforeInsert.RelatedQuery { - private BeforeInsert.RecordsProvider provider; - - public AccountNamePopulatorWithRelatedQuery(BeforeInsert.RecordsProvider provider) { - this.provider = provider; - } - - public Map queryParentsOnBeforeInsert() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Map queryRelatedOnBeforeInsert() { - return new Map{ 'accountNames' => this.provider }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, record.getRelated('accountNames').isEmpty() ? 'None' : 'Some'); - } - } - - private class FailingRelatedQueryPopulatorWithContinueOnError implements BeforeInsert.Populator, BeforeInsert.RelatedQuery, BeforeInsert.ContinueOnError { - public Map queryRelatedOnBeforeInsert() { - throw new TestException('The related query failed.'); - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, record.getRelated('accounts').isEmpty() ? 'CEO' : 'CFO'); - } - } - - private class DmlRecordInsertWriter implements AfterInsert.Writer { - private Account accountToInsert; - - public DmlRecordInsertWriter(Account accountToInsert) { - this.accountToInsert = accountToInsert; - } - - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(DML.Record(this.accountToInsert)); - } - } - - private class AccountUpdateWriter implements AfterInsert.Writer { - private Account accountToUpdate; - - public AccountUpdateWriter(Account accountToUpdate) { - this.accountToUpdate = accountToUpdate; - } - - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toUpdate(this.accountToUpdate); - } - } - - private class DmlRecordUpdateWriter implements AfterInsert.Writer { - private Account accountToUpdate; - - public DmlRecordUpdateWriter(Account accountToUpdate) { - this.accountToUpdate = accountToUpdate; - } - - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toUpdate(DML.Record(this.accountToUpdate)); - } - } - - private class AccountUpsertWriter implements AfterInsert.Writer { - private Account accountToUpsert; - - public AccountUpsertWriter(Account accountToUpsert) { - this.accountToUpsert = accountToUpsert; - } - - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toUpsert(this.accountToUpsert, Account.Id); - } - } - - private class AccountDeleteWriter implements AfterInsert.Writer { - private Account accountToDelete; - - public AccountDeleteWriter(Account accountToDelete) { - this.accountToDelete = accountToDelete; - } - - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toDelete(this.accountToDelete); - } - } - - private class EventPublishWriter implements AfterInsert.Writer { - private SObject eventToPublish; - - public EventPublishWriter(SObject eventToPublish) { - this.eventToPublish = eventToPublish; - } - - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toPublish(this.eventToPublish); - } - } - - private class UndeclaredIndustryPopulator implements BeforeInsert.Populator, BeforeInsert.ParentQuery { - public Map queryParentsOnBeforeInsert() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Description, record.getNewParent(Contact.AccountId)?.get(Account.Industry)); - } - } - - private class AccountOwnerPopulator implements BeforeInsert.Populator, BeforeInsert.ParentQuery { - public Map queryParentsOnBeforeInsert() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with('Owner', User.LastName) }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Description, record.getNewParent(Contact.AccountId)?.getSObject('Owner')?.get(User.LastName)); - } - } - - private class EmployeeCounter implements BeforeInsert.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return true; - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Account.NumberOfEmployees, ((Account) record.getNewSObject()).NumberOfEmployees + 1); - } - } - - private class TestOrchestrator implements TriggerOrchestrator.BeforeInsert, TriggerOrchestrator.AfterInsert, TriggerOrchestrator.BeforeUpdate, TriggerOrchestrator.AfterUpdate, TriggerOrchestrator.BeforeDelete, TriggerOrchestrator.AfterDelete, TriggerOrchestrator.AfterUndelete { - public List beforeInsert = new List(); - public List afterInsert = new List(); - public List beforeUpdate = new List(); - public List afterUpdate = new List(); - public List beforeDelete = new List(); - public List afterDelete = new List(); - public List afterUndelete = new List(); - - public List beforeInsertHandlers() { - return this.beforeInsert; - } - - public List afterInsertHandlers() { - return this.afterInsert; - } - - public List beforeUpdateHandlers() { - return this.beforeUpdate; - } - - public List afterUpdateHandlers() { - return this.afterUpdate; - } - - public List beforeDeleteHandlers() { - return this.beforeDelete; - } - - public List afterDeleteHandlers() { - return this.afterDelete; - } - - public List afterUndeleteHandlers() { - return this.afterUndelete; - } - } - - private virtual class TitlePopulator implements BeforeInsert.Populator, BeforeUpdate.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Title, 'CEO'); - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Title, 'CEO'); - } - } - - private virtual class DoeValidator implements BeforeInsert.Validator, BeforeUpdate.Validator { - public Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record) { - record.addError('Doe is not allowed.'); - } - - public Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record) { - record.addError('Doe is not allowed.'); - } - } - - private virtual class AccountWriter implements AfterInsert.Writer, AfterUpdate.Writer, BeforeDelete.Writer, AfterDelete.Writer, AfterUndelete.Writer { - public Boolean writeOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterInsert(TriggerTypes.InsertRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - } - - public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - } - - public Boolean writeOnBeforeDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnBeforeDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getOldSObject().get(Contact.LastName))); - } - - public Boolean writeOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterDelete(TriggerTypes.DeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getOldSObject().get(Contact.LastName))); - } - - public Boolean writeOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void writeOnAfterUndelete(TriggerTypes.UndeleteRecord record, TriggerTypes.UnitOfWork unitOfWork) { - unitOfWork.toInsert(new Account(Name = (String) record.getNewSObject().get(Contact.LastName))); - } - } - - private virtual class IdDispatcher implements AfterInsert.Dispatcher, AfterUpdate.Dispatcher, AfterDelete.Dispatcher, AfterUndelete.Dispatcher { - public List dispatchedIds = new List(); - - public Boolean dispatchOnAfterInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterInsert(TriggerTypes.InsertRecords records) { - this.dispatchedIds.addAll(records.getIds()); - } - - public Boolean dispatchOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUpdate(TriggerTypes.UpdateRecords records) { - this.dispatchedIds.addAll(records.getIds()); - } - - public Boolean dispatchOnAfterDeleteWhen(TriggerTypes.DeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterDelete(TriggerTypes.DeleteRecords records) { - this.dispatchedIds.addAll(records.getIds()); - } - - public Boolean dispatchOnAfterUndeleteWhen(TriggerTypes.UndeleteRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void dispatchOnAfterUndelete(TriggerTypes.UndeleteRecords records) { - this.dispatchedIds.addAll(records.getIds()); - } - } - - private virtual class AccountNamePopulator implements BeforeInsert.Populator, BeforeInsert.ParentQuery, BeforeUpdate.Populator, BeforeUpdate.ParentQuery { - public Map queryParentsOnBeforeInsert() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - record.put(Contact.Department, record.getNewParent(Contact.AccountId)?.get(Account.Name)); - } - - public Map queryParentsOnBeforeUpdate() { - return new Map{ Contact.AccountId => TriggerTypes.ParentFields.with(Account.Name) }; - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - record.put(Contact.Department, record.getNewParent(Contact.AccountId)?.get(Account.Name)); - } - } - - private virtual class FailingPopulator implements BeforeInsert.Populator, BeforeUpdate.Populator { - public Boolean populateOnBeforeInsertWhen(TriggerTypes.InsertRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeInsert(TriggerTypes.InsertRecord record) { - throw new TestException('The populator failed.'); - } - - public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) { - return record.equals(Contact.LastName, 'Doe'); - } - - public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) { - throw new TestException('The populator failed.'); - } - } - - private class FailingPopulatorWithContinueOnError extends FailingPopulator implements BeforeInsert.ContinueOnError, BeforeUpdate.ContinueOnError { - } - - public class TestLogger implements TriggerOrchestrator.Logger { - public List errors = new List(); - public Integer flushCalls = 0; - - public void log(TriggerOrchestrator.Error error) { - this.errors.add(error); - } - - public void flush() { - this.flushCalls++; - } - } - - private class TestException extends Exception { - } -} diff --git a/package/main/default/classes/TriggerOrchestratorTest.cls-meta.xml b/package/main/default/classes/TriggerOrchestratorTest.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/TriggerOrchestratorTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/TriggerTypes.cls b/package/main/default/classes/TriggerTypes.cls deleted file mode 100644 index 82ed805..0000000 --- a/package/main/default/classes/TriggerTypes.cls +++ /dev/null @@ -1,1529 +0,0 @@ -/** - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - ExcessivePublicCount: It is a library class and exposes all necessary methods to evaluate a trigger record - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE class - * - ExcessiveParameterList: Fixed arity overloads spare the caller from building a list for the common cases - * - PropertyNamingConventions: It was intentional to make the lib more fluent and readable - * - FieldNamingConventions: It was intentional to make the lib more fluent and readable - * - FieldDeclarationsShouldBeAtStart: Developer who uses lib should see what's important at start - **/ -@SuppressWarnings( - 'PMD.ExcessivePublicCount,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.ExcessiveParameterList,PMD.PropertyNamingConventions,PMD.FieldNamingConventions,PMD.FieldDeclarationsShouldBeAtStart' -) -global with sharing class TriggerTypes { - global static ParentFields ParentFields { - get { - return new TriggerParentFields(); - } - } - - global interface InsertRecord { - Boolean isRecordType(String recordTypeDeveloperName); - Boolean isNotRecordType(String recordTypeDeveloperName); - - Boolean equals(SObjectField field, Object value); - Boolean doesNotEqual(SObjectField field, Object value); - - Boolean isIn(SObjectField field, Iterable values); - Boolean isNotIn(SObjectField field, Iterable values); - - Boolean contains(SObjectField field, String value); - Boolean doesNotContain(SObjectField field, String value); - Boolean startsWith(SObjectField field, String value); - Boolean endsWith(SObjectField field, String value); - - Boolean isNull(SObjectField field); - Boolean isNotNull(SObjectField field); - - Boolean isEmpty(SObjectField field); - Boolean isNotEmpty(SObjectField field); - - Boolean isBlank(SObjectField field); - Boolean isNotBlank(SObjectField field); - - Boolean isTrue(SObjectField field); - Boolean isFalse(SObjectField field); - - Boolean greaterThan(SObjectField field, Integer value); - Boolean greaterThan(SObjectField field, DateTime value); - Boolean greaterThan(SObjectField field, Date value); - Boolean greaterThan(SObjectField field, Double value); - Boolean greaterThan(SObjectField field, Decimal value); - Boolean greaterThan(SObjectField field, Long value); - - Boolean greaterThanOrEqualTo(SObjectField field, Integer value); - Boolean greaterThanOrEqualTo(SObjectField field, DateTime value); - Boolean greaterThanOrEqualTo(SObjectField field, Date value); - Boolean greaterThanOrEqualTo(SObjectField field, Double value); - Boolean greaterThanOrEqualTo(SObjectField field, Decimal value); - Boolean greaterThanOrEqualTo(SObjectField field, Long value); - - Boolean lessThan(SObjectField field, Integer value); - Boolean lessThan(SObjectField field, DateTime value); - Boolean lessThan(SObjectField field, Date value); - Boolean lessThan(SObjectField field, Double value); - Boolean lessThan(SObjectField field, Decimal value); - Boolean lessThan(SObjectField field, Long value); - - Boolean lessThanOrEqualTo(SObjectField field, Integer value); - Boolean lessThanOrEqualTo(SObjectField field, DateTime value); - Boolean lessThanOrEqualTo(SObjectField field, Date value); - Boolean lessThanOrEqualTo(SObjectField field, Double value); - Boolean lessThanOrEqualTo(SObjectField field, Decimal value); - Boolean lessThanOrEqualTo(SObjectField field, Long value); - - Id getId(); - - SObject getNewSObject(); - - SObject getNewParent(SObjectField lookupField); - - TriggerTypes.RelatedRecords getRelated(String providerName); - - void put(SObjectField field, Object value); - } - - global interface RejectableInsertRecord { - Boolean isRecordType(String recordTypeDeveloperName); - Boolean isNotRecordType(String recordTypeDeveloperName); - - Boolean equals(SObjectField field, Object value); - Boolean doesNotEqual(SObjectField field, Object value); - - Boolean isIn(SObjectField field, Iterable values); - Boolean isNotIn(SObjectField field, Iterable values); - - Boolean contains(SObjectField field, String value); - Boolean doesNotContain(SObjectField field, String value); - Boolean startsWith(SObjectField field, String value); - Boolean endsWith(SObjectField field, String value); - - Boolean isNull(SObjectField field); - Boolean isNotNull(SObjectField field); - - Boolean isEmpty(SObjectField field); - Boolean isNotEmpty(SObjectField field); - - Boolean isBlank(SObjectField field); - Boolean isNotBlank(SObjectField field); - - Boolean isTrue(SObjectField field); - Boolean isFalse(SObjectField field); - - Boolean greaterThan(SObjectField field, Integer value); - Boolean greaterThan(SObjectField field, DateTime value); - Boolean greaterThan(SObjectField field, Date value); - Boolean greaterThan(SObjectField field, Double value); - Boolean greaterThan(SObjectField field, Decimal value); - Boolean greaterThan(SObjectField field, Long value); - - Boolean greaterThanOrEqualTo(SObjectField field, Integer value); - Boolean greaterThanOrEqualTo(SObjectField field, DateTime value); - Boolean greaterThanOrEqualTo(SObjectField field, Date value); - Boolean greaterThanOrEqualTo(SObjectField field, Double value); - Boolean greaterThanOrEqualTo(SObjectField field, Decimal value); - Boolean greaterThanOrEqualTo(SObjectField field, Long value); - - Boolean lessThan(SObjectField field, Integer value); - Boolean lessThan(SObjectField field, DateTime value); - Boolean lessThan(SObjectField field, Date value); - Boolean lessThan(SObjectField field, Double value); - Boolean lessThan(SObjectField field, Decimal value); - Boolean lessThan(SObjectField field, Long value); - - Boolean lessThanOrEqualTo(SObjectField field, Integer value); - Boolean lessThanOrEqualTo(SObjectField field, DateTime value); - Boolean lessThanOrEqualTo(SObjectField field, Date value); - Boolean lessThanOrEqualTo(SObjectField field, Double value); - Boolean lessThanOrEqualTo(SObjectField field, Decimal value); - Boolean lessThanOrEqualTo(SObjectField field, Long value); - - Id getId(); - - SObject getNewSObject(); - - SObject getNewParent(SObjectField lookupField); - - TriggerTypes.RelatedRecords getRelated(String providerName); - - void addError(String error); - void addError(SObjectField field, String error); - } - - global interface UpdateRecord { - Boolean isRecordType(String recordTypeDeveloperName); - Boolean isNotRecordType(String recordTypeDeveloperName); - - Boolean equals(SObjectField field, Object value); - Boolean doesNotEqual(SObjectField field, Object value); - - Boolean isIn(SObjectField field, Iterable values); - Boolean isNotIn(SObjectField field, Iterable values); - - Boolean contains(SObjectField field, String value); - Boolean doesNotContain(SObjectField field, String value); - Boolean startsWith(SObjectField field, String value); - Boolean endsWith(SObjectField field, String value); - - Boolean isNull(SObjectField field); - Boolean isNotNull(SObjectField field); - - Boolean isEmpty(SObjectField field); - Boolean isNotEmpty(SObjectField field); - - Boolean isBlank(SObjectField field); - Boolean isNotBlank(SObjectField field); - - Boolean isChanged(SObjectField field); - - Boolean isAnyChanged(SObjectField field1, SObjectField field2); - Boolean isAnyChanged(SObjectField field1, SObjectField field2, SObjectField field3); - Boolean isAnyChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - Boolean isAnyChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - Boolean isAnyChanged(Iterable fields); - - Boolean areAllChanged(SObjectField field1, SObjectField field2); - Boolean areAllChanged(SObjectField field1, SObjectField field2, SObjectField field3); - Boolean areAllChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - Boolean areAllChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - Boolean areAllChanged(Iterable fields); - - Boolean isChangedTo(SObjectField field, Object expectedValue); - Boolean isChangedToNotNull(SObjectField field); - Boolean isChangedFrom(SObjectField field, Object priorValue); - Boolean isChangedFromTo(SObjectField field, Object fromValue, Object toValue); - - Boolean isTrue(SObjectField field); - Boolean isFalse(SObjectField field); - - Boolean greaterThan(SObjectField field, Integer value); - Boolean greaterThan(SObjectField field, DateTime value); - Boolean greaterThan(SObjectField field, Date value); - Boolean greaterThan(SObjectField field, Double value); - Boolean greaterThan(SObjectField field, Decimal value); - Boolean greaterThan(SObjectField field, Long value); - - Boolean greaterThanOrEqualTo(SObjectField field, Integer value); - Boolean greaterThanOrEqualTo(SObjectField field, DateTime value); - Boolean greaterThanOrEqualTo(SObjectField field, Date value); - Boolean greaterThanOrEqualTo(SObjectField field, Double value); - Boolean greaterThanOrEqualTo(SObjectField field, Decimal value); - Boolean greaterThanOrEqualTo(SObjectField field, Long value); - - Boolean lessThan(SObjectField field, Integer value); - Boolean lessThan(SObjectField field, DateTime value); - Boolean lessThan(SObjectField field, Date value); - Boolean lessThan(SObjectField field, Double value); - Boolean lessThan(SObjectField field, Decimal value); - Boolean lessThan(SObjectField field, Long value); - - Boolean lessThanOrEqualTo(SObjectField field, Integer value); - Boolean lessThanOrEqualTo(SObjectField field, DateTime value); - Boolean lessThanOrEqualTo(SObjectField field, Date value); - Boolean lessThanOrEqualTo(SObjectField field, Double value); - Boolean lessThanOrEqualTo(SObjectField field, Decimal value); - Boolean lessThanOrEqualTo(SObjectField field, Long value); - - Id getId(); - - SObject getNewSObject(); - SObject getOldSObject(); - - SObject getNewParent(SObjectField lookupField); - SObject getOldParent(SObjectField lookupField); - - TriggerTypes.RelatedRecords getRelated(String providerName); - - void put(SObjectField field, Object value); - } - - global interface RejectableUpdateRecord { - Boolean isRecordType(String recordTypeDeveloperName); - Boolean isNotRecordType(String recordTypeDeveloperName); - - Boolean equals(SObjectField field, Object value); - Boolean doesNotEqual(SObjectField field, Object value); - - Boolean isIn(SObjectField field, Iterable values); - Boolean isNotIn(SObjectField field, Iterable values); - - Boolean contains(SObjectField field, String value); - Boolean doesNotContain(SObjectField field, String value); - Boolean startsWith(SObjectField field, String value); - Boolean endsWith(SObjectField field, String value); - - Boolean isNull(SObjectField field); - Boolean isNotNull(SObjectField field); - - Boolean isEmpty(SObjectField field); - Boolean isNotEmpty(SObjectField field); - - Boolean isBlank(SObjectField field); - Boolean isNotBlank(SObjectField field); - - Boolean isChanged(SObjectField field); - - Boolean isAnyChanged(SObjectField field1, SObjectField field2); - Boolean isAnyChanged(SObjectField field1, SObjectField field2, SObjectField field3); - Boolean isAnyChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - Boolean isAnyChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - Boolean isAnyChanged(Iterable fields); - - Boolean areAllChanged(SObjectField field1, SObjectField field2); - Boolean areAllChanged(SObjectField field1, SObjectField field2, SObjectField field3); - Boolean areAllChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - Boolean areAllChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - Boolean areAllChanged(Iterable fields); - - Boolean isChangedTo(SObjectField field, Object expectedValue); - Boolean isChangedToNotNull(SObjectField field); - Boolean isChangedFrom(SObjectField field, Object priorValue); - Boolean isChangedFromTo(SObjectField field, Object fromValue, Object toValue); - - Boolean isTrue(SObjectField field); - Boolean isFalse(SObjectField field); - - Boolean greaterThan(SObjectField field, Integer value); - Boolean greaterThan(SObjectField field, DateTime value); - Boolean greaterThan(SObjectField field, Date value); - Boolean greaterThan(SObjectField field, Double value); - Boolean greaterThan(SObjectField field, Decimal value); - Boolean greaterThan(SObjectField field, Long value); - - Boolean greaterThanOrEqualTo(SObjectField field, Integer value); - Boolean greaterThanOrEqualTo(SObjectField field, DateTime value); - Boolean greaterThanOrEqualTo(SObjectField field, Date value); - Boolean greaterThanOrEqualTo(SObjectField field, Double value); - Boolean greaterThanOrEqualTo(SObjectField field, Decimal value); - Boolean greaterThanOrEqualTo(SObjectField field, Long value); - - Boolean lessThan(SObjectField field, Integer value); - Boolean lessThan(SObjectField field, DateTime value); - Boolean lessThan(SObjectField field, Date value); - Boolean lessThan(SObjectField field, Double value); - Boolean lessThan(SObjectField field, Decimal value); - Boolean lessThan(SObjectField field, Long value); - - Boolean lessThanOrEqualTo(SObjectField field, Integer value); - Boolean lessThanOrEqualTo(SObjectField field, DateTime value); - Boolean lessThanOrEqualTo(SObjectField field, Date value); - Boolean lessThanOrEqualTo(SObjectField field, Double value); - Boolean lessThanOrEqualTo(SObjectField field, Decimal value); - Boolean lessThanOrEqualTo(SObjectField field, Long value); - - Id getId(); - - SObject getNewSObject(); - SObject getOldSObject(); - - SObject getNewParent(SObjectField lookupField); - SObject getOldParent(SObjectField lookupField); - - TriggerTypes.RelatedRecords getRelated(String providerName); - - void addError(String error); - void addError(SObjectField field, String error); - } - - global interface DeleteRecord { - Boolean isRecordType(String recordTypeDeveloperName); - Boolean isNotRecordType(String recordTypeDeveloperName); - - Boolean equals(SObjectField field, Object value); - Boolean doesNotEqual(SObjectField field, Object value); - - Boolean isIn(SObjectField field, Iterable values); - Boolean isNotIn(SObjectField field, Iterable values); - - Boolean contains(SObjectField field, String value); - Boolean doesNotContain(SObjectField field, String value); - Boolean startsWith(SObjectField field, String value); - Boolean endsWith(SObjectField field, String value); - - Boolean isNull(SObjectField field); - Boolean isNotNull(SObjectField field); - - Boolean isEmpty(SObjectField field); - Boolean isNotEmpty(SObjectField field); - - Boolean isBlank(SObjectField field); - Boolean isNotBlank(SObjectField field); - - Boolean isTrue(SObjectField field); - Boolean isFalse(SObjectField field); - - Boolean greaterThan(SObjectField field, Integer value); - Boolean greaterThan(SObjectField field, DateTime value); - Boolean greaterThan(SObjectField field, Date value); - Boolean greaterThan(SObjectField field, Double value); - Boolean greaterThan(SObjectField field, Decimal value); - Boolean greaterThan(SObjectField field, Long value); - - Boolean greaterThanOrEqualTo(SObjectField field, Integer value); - Boolean greaterThanOrEqualTo(SObjectField field, DateTime value); - Boolean greaterThanOrEqualTo(SObjectField field, Date value); - Boolean greaterThanOrEqualTo(SObjectField field, Double value); - Boolean greaterThanOrEqualTo(SObjectField field, Decimal value); - Boolean greaterThanOrEqualTo(SObjectField field, Long value); - - Boolean lessThan(SObjectField field, Integer value); - Boolean lessThan(SObjectField field, DateTime value); - Boolean lessThan(SObjectField field, Date value); - Boolean lessThan(SObjectField field, Double value); - Boolean lessThan(SObjectField field, Decimal value); - Boolean lessThan(SObjectField field, Long value); - - Boolean lessThanOrEqualTo(SObjectField field, Integer value); - Boolean lessThanOrEqualTo(SObjectField field, DateTime value); - Boolean lessThanOrEqualTo(SObjectField field, Date value); - Boolean lessThanOrEqualTo(SObjectField field, Double value); - Boolean lessThanOrEqualTo(SObjectField field, Decimal value); - Boolean lessThanOrEqualTo(SObjectField field, Long value); - - Id getId(); - - SObject getOldSObject(); - - SObject getOldParent(SObjectField lookupField); - - TriggerTypes.RelatedRecords getRelated(String providerName); - } - - global interface RejectableDeleteRecord { - Boolean isRecordType(String recordTypeDeveloperName); - Boolean isNotRecordType(String recordTypeDeveloperName); - - Boolean equals(SObjectField field, Object value); - Boolean doesNotEqual(SObjectField field, Object value); - - Boolean isIn(SObjectField field, Iterable values); - Boolean isNotIn(SObjectField field, Iterable values); - - Boolean contains(SObjectField field, String value); - Boolean doesNotContain(SObjectField field, String value); - Boolean startsWith(SObjectField field, String value); - Boolean endsWith(SObjectField field, String value); - - Boolean isNull(SObjectField field); - Boolean isNotNull(SObjectField field); - - Boolean isEmpty(SObjectField field); - Boolean isNotEmpty(SObjectField field); - - Boolean isBlank(SObjectField field); - Boolean isNotBlank(SObjectField field); - - Boolean isTrue(SObjectField field); - Boolean isFalse(SObjectField field); - - Boolean greaterThan(SObjectField field, Integer value); - Boolean greaterThan(SObjectField field, DateTime value); - Boolean greaterThan(SObjectField field, Date value); - Boolean greaterThan(SObjectField field, Double value); - Boolean greaterThan(SObjectField field, Decimal value); - Boolean greaterThan(SObjectField field, Long value); - - Boolean greaterThanOrEqualTo(SObjectField field, Integer value); - Boolean greaterThanOrEqualTo(SObjectField field, DateTime value); - Boolean greaterThanOrEqualTo(SObjectField field, Date value); - Boolean greaterThanOrEqualTo(SObjectField field, Double value); - Boolean greaterThanOrEqualTo(SObjectField field, Decimal value); - Boolean greaterThanOrEqualTo(SObjectField field, Long value); - - Boolean lessThan(SObjectField field, Integer value); - Boolean lessThan(SObjectField field, DateTime value); - Boolean lessThan(SObjectField field, Date value); - Boolean lessThan(SObjectField field, Double value); - Boolean lessThan(SObjectField field, Decimal value); - Boolean lessThan(SObjectField field, Long value); - - Boolean lessThanOrEqualTo(SObjectField field, Integer value); - Boolean lessThanOrEqualTo(SObjectField field, DateTime value); - Boolean lessThanOrEqualTo(SObjectField field, Date value); - Boolean lessThanOrEqualTo(SObjectField field, Double value); - Boolean lessThanOrEqualTo(SObjectField field, Decimal value); - Boolean lessThanOrEqualTo(SObjectField field, Long value); - - Id getId(); - - SObject getOldSObject(); - - SObject getOldParent(SObjectField lookupField); - - TriggerTypes.RelatedRecords getRelated(String providerName); - - void addError(String error); - void addError(SObjectField field, String error); - } - - global interface UndeleteRecord { - Boolean isRecordType(String recordTypeDeveloperName); - Boolean isNotRecordType(String recordTypeDeveloperName); - - Boolean equals(SObjectField field, Object value); - Boolean doesNotEqual(SObjectField field, Object value); - - Boolean isIn(SObjectField field, Iterable values); - Boolean isNotIn(SObjectField field, Iterable values); - - Boolean contains(SObjectField field, String value); - Boolean doesNotContain(SObjectField field, String value); - Boolean startsWith(SObjectField field, String value); - Boolean endsWith(SObjectField field, String value); - - Boolean isNull(SObjectField field); - Boolean isNotNull(SObjectField field); - - Boolean isEmpty(SObjectField field); - Boolean isNotEmpty(SObjectField field); - - Boolean isBlank(SObjectField field); - Boolean isNotBlank(SObjectField field); - - Boolean isTrue(SObjectField field); - Boolean isFalse(SObjectField field); - - Boolean greaterThan(SObjectField field, Integer value); - Boolean greaterThan(SObjectField field, DateTime value); - Boolean greaterThan(SObjectField field, Date value); - Boolean greaterThan(SObjectField field, Double value); - Boolean greaterThan(SObjectField field, Decimal value); - Boolean greaterThan(SObjectField field, Long value); - - Boolean greaterThanOrEqualTo(SObjectField field, Integer value); - Boolean greaterThanOrEqualTo(SObjectField field, DateTime value); - Boolean greaterThanOrEqualTo(SObjectField field, Date value); - Boolean greaterThanOrEqualTo(SObjectField field, Double value); - Boolean greaterThanOrEqualTo(SObjectField field, Decimal value); - Boolean greaterThanOrEqualTo(SObjectField field, Long value); - - Boolean lessThan(SObjectField field, Integer value); - Boolean lessThan(SObjectField field, DateTime value); - Boolean lessThan(SObjectField field, Date value); - Boolean lessThan(SObjectField field, Double value); - Boolean lessThan(SObjectField field, Decimal value); - Boolean lessThan(SObjectField field, Long value); - - Boolean lessThanOrEqualTo(SObjectField field, Integer value); - Boolean lessThanOrEqualTo(SObjectField field, DateTime value); - Boolean lessThanOrEqualTo(SObjectField field, Date value); - Boolean lessThanOrEqualTo(SObjectField field, Double value); - Boolean lessThanOrEqualTo(SObjectField field, Decimal value); - Boolean lessThanOrEqualTo(SObjectField field, Long value); - - Id getId(); - - SObject getNewSObject(); - - SObject getNewParent(SObjectField lookupField); - - TriggerTypes.RelatedRecords getRelated(String providerName); - } - - global interface InsertRecords { - Set getIds(); - - Set getIdsOf(SObjectField field); - Set getIdsOf(String relationshipName, SObjectField field); - - Set getValuesOf(SObjectField field); - Set getValuesOf(String relationshipName, SObjectField field); - - List getRecords(); - - Integer size(); - } - - global interface UpdateRecords { - Set getIds(); - - Set getIdsOf(SObjectField field); - Set getIdsOf(String relationshipName, SObjectField field); - Set getOldIdsOf(SObjectField field); - Set getOldIdsOf(String relationshipName, SObjectField field); - - Set getValuesOf(SObjectField field); - Set getValuesOf(String relationshipName, SObjectField field); - Set getOldValuesOf(SObjectField field); - Set getOldValuesOf(String relationshipName, SObjectField field); - - List getRecords(); - - Integer size(); - } - - global interface DeleteRecords { - Set getIds(); - - Set getIdsOf(SObjectField field); - Set getIdsOf(String relationshipName, SObjectField field); - - Set getValuesOf(SObjectField field); - Set getValuesOf(String relationshipName, SObjectField field); - - List getRecords(); - - Integer size(); - } - - global interface UndeleteRecords { - Set getIds(); - - Set getIdsOf(SObjectField field); - Set getIdsOf(String relationshipName, SObjectField field); - - Set getValuesOf(SObjectField field); - Set getValuesOf(String relationshipName, SObjectField field); - - List getRecords(); - - Integer size(); - } - - global interface UnitOfWork { - UnitOfWork toInsert(SObject record); - UnitOfWork toInsert(DML.Record record); - - UnitOfWork toUpdate(SObject record); - UnitOfWork toUpdate(DML.Record record); - - UnitOfWork toUpsert(SObject record, SObjectField externalIdField); - - UnitOfWork toDelete(SObject record); - - UnitOfWork toPublish(SObject event); - } - - global interface ParentFields { - ParentFields with(SObjectField field); - ParentFields with(SObjectField field1, SObjectField field2); - ParentFields with(SObjectField field1, SObjectField field2, SObjectField field3); - ParentFields with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - ParentFields with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - ParentFields with(Iterable fields); - // Relationship fields - ParentFields with(String relationshipName, SObjectField field); - ParentFields with(String relationshipName, SObjectField field1, SObjectField field2); - ParentFields with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3); - ParentFields with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4); - ParentFields with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5); - ParentFields with(String relationshipName, Iterable fields); - - // for private use only - List getFields(); - } - - global interface RelatedRecords { - SObject getFirstWhereKeyEquals(Object key); - - List getAllWhereKeyEquals(Object key); - List getRecords(); - - Boolean isEmpty(); - } - - public interface ParentLoader { - void load(String relationshipName); - } - - public interface RelatedLoader { - void load(String providerName); - } - - // Implementation - - private static RecordTypesInfo RecordTypesInfo = new RecordTypesInfo(); - private static RecordsAggregator RecordsAggregator = new RecordsAggregator(); - - public static List toTriggerRecords(List newRecords, List oldRecords) { - List triggerRecords = new List(); - - Integer recordCount = (newRecords ?? oldRecords)?.size() ?? 0; - - for (Integer i = 0; i < recordCount; i++) { - triggerRecords.add(new TriggerRecord(newRecords?.get(i), oldRecords?.get(i))); - } - - return triggerRecords; - } - - public class TriggerRecord implements TriggerTypes.InsertRecord, TriggerTypes.RejectableInsertRecord, TriggerTypes.UpdateRecord, TriggerTypes.RejectableUpdateRecord, TriggerTypes.DeleteRecord, TriggerTypes.RejectableDeleteRecord, TriggerTypes.UndeleteRecord { - private SObject newRecord; - private SObject oldRecord; - - private Map newParents = new Map(); - private Map oldParents = new Map(); - - private Map relatedByProviderName = new Map(); - - private ParentLoader parentLoader; - private RelatedLoader relatedLoader; - - private SObjectType sObjectType; - - public TriggerRecord(SObject newRow, SObject oldRow) { - this.newRecord = newRow; - this.oldRecord = oldRow; - this.sObjectType = (newRow ?? oldRow).getSObjectType(); - } - - public Boolean isRecordType(String recordTypeDeveloperName) { - if (!RecordTypesInfo.hasRecordTypes(this.sObjectType)) { - throw new TriggerLibException(this.sObjectType.toString() + ' has no record types, so isRecordType cannot be used on it.'); - } - - return RecordTypesInfo.getId(this.sObjectType, recordTypeDeveloperName) == (this.newRecord ?? this.oldRecord).get('RecordTypeId'); - } - - public Boolean isNotRecordType(String recordTypeDeveloperName) { - if (!RecordTypesInfo.hasRecordTypes(this.sObjectType)) { - throw new TriggerLibException(this.sObjectType.toString() + ' has no record types, so isNotRecordType cannot be used on it.'); - } - - return RecordTypesInfo.getId(this.sObjectType, recordTypeDeveloperName) != (this.newRecord ?? this.oldRecord).get('RecordTypeId'); - } - - public Boolean equals(SObjectField field, Object value) { - return this.getFieldValue(field) == value; - } - - public Boolean doesNotEqual(SObjectField field, Object value) { - return !this.equals(field, value); - } - - public Boolean isIn(SObjectField field, Iterable values) { - for (Object value : values) { - if (this.equals(field, value)) { - return true; - } - } - - return false; - } - - public Boolean isNotIn(SObjectField field, Iterable values) { - return !this.isIn(field, values); - } - - public Boolean contains(SObjectField field, String value) { - return this.isNotNull(field) && this.getFieldValue(field).toString().contains(value); - } - - public Boolean doesNotContain(SObjectField field, String value) { - return this.getFieldValue(field) == null || !this.getFieldValue(field).toString().contains(value); - } - - public Boolean startsWith(SObjectField field, String value) { - return this.isNotNull(field) && this.getFieldValue(field).toString().startsWith(value); - } - - public Boolean endsWith(SObjectField field, String value) { - return this.isNotNull(field) && this.getFieldValue(field).toString().endsWith(value); - } - - public Boolean isNull(SObjectField field) { - return this.getFieldValue(field) == null; - } - - public Boolean isNotNull(SObjectField field) { - return !this.isNull(field); - } - - public Boolean isEmpty(SObjectField field) { - return this.isNull(field) || String.isEmpty(this.getFieldValue(field).toString()); - } - - public Boolean isNotEmpty(SObjectField field) { - return !this.isEmpty(field); - } - - public Boolean isBlank(SObjectField field) { - return this.isNull(field) || String.isBlank(this.getFieldValue(field).toString()); - } - - public Boolean isNotBlank(SObjectField field) { - return !this.isBlank(field); - } - - public Boolean isChanged(SObjectField field) { - return this.getFieldValue(field) != this.getOldFieldValue(field); - } - - public Boolean isAnyChanged(SObjectField field1, SObjectField field2) { - return this.isAnyChanged(new List{ field1, field2 }); - } - - public Boolean isAnyChanged(SObjectField field1, SObjectField field2, SObjectField field3) { - return this.isAnyChanged(new List{ field1, field2, field3 }); - } - - public Boolean isAnyChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { - return this.isAnyChanged(new List{ field1, field2, field3, field4 }); - } - - public Boolean isAnyChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { - return this.isAnyChanged(new List{ field1, field2, field3, field4, field5 }); - } - - public Boolean isAnyChanged(Iterable fields) { - for (SObjectField field : fields) { - if (this.isChanged(field)) { - return true; - } - } - - return false; - } - - public Boolean areAllChanged(SObjectField field1, SObjectField field2) { - return this.areAllChanged(new List{ field1, field2 }); - } - - public Boolean areAllChanged(SObjectField field1, SObjectField field2, SObjectField field3) { - return this.areAllChanged(new List{ field1, field2, field3 }); - } - - public Boolean areAllChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { - return this.areAllChanged(new List{ field1, field2, field3, field4 }); - } - - public Boolean areAllChanged(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { - return this.areAllChanged(new List{ field1, field2, field3, field4, field5 }); - } - - public Boolean areAllChanged(Iterable fields) { - for (SObjectField field : fields) { - if (!this.isChanged(field)) { - return false; - } - } - - return true; - } - - public Boolean isChangedTo(SObjectField field, Object value) { - return this.getFieldValue(field) == value && this.getOldFieldValue(field) != value; - } - - public Boolean isChangedToNotNull(SObjectField field) { - return this.isNotNull(field) && this.isChanged(field); - } - - public Boolean isChangedFrom(SObjectField field, Object value) { - return this.getFieldValue(field) != value && this.getOldFieldValue(field) == value; - } - - public Boolean isChangedFromTo(SObjectField field, Object fromValue, Object toValue) { - return this.getFieldValue(field) == toValue && this.getOldFieldValue(field) == fromValue; - } - - public Boolean isTrue(SObjectField field) { - return this.getFieldValue(field) == true; - } - - public Boolean isFalse(SObjectField field) { - return this.getFieldValue(field) == false; - } - - public Boolean greaterThan(SObjectField field, Integer value) { - return this.isGreaterThan(field, value); - } - - public Boolean greaterThan(SObjectField field, Long value) { - return this.isGreaterThan(field, value); - } - - public Boolean greaterThan(SObjectField field, Double value) { - return this.isGreaterThan(field, value); - } - - public Boolean greaterThan(SObjectField field, Decimal value) { - return this.isGreaterThan(field, value); - } - - public Boolean greaterThan(SObjectField field, DateTime value) { - Object fieldValue = this.getFieldValue(field); - return fieldValue != null && ((DateTime) fieldValue) > value; - } - - public Boolean greaterThan(SObjectField field, Date value) { - Object fieldValue = this.getFieldValue(field); - return fieldValue != null && ((Date) fieldValue) > value; - } - - public Boolean greaterThanOrEqualTo(SObjectField field, Integer value) { - return this.isGreaterThanOrEqualTo(field, value); - } - - public Boolean greaterThanOrEqualTo(SObjectField field, Long value) { - return this.isGreaterThanOrEqualTo(field, value); - } - - public Boolean greaterThanOrEqualTo(SObjectField field, Double value) { - return this.isGreaterThanOrEqualTo(field, value); - } - - public Boolean greaterThanOrEqualTo(SObjectField field, Decimal value) { - return this.isGreaterThanOrEqualTo(field, value); - } - - public Boolean greaterThanOrEqualTo(SObjectField field, DateTime value) { - Object fieldValue = this.getFieldValue(field); - return fieldValue != null && ((DateTime) fieldValue) >= value; - } - - public Boolean greaterThanOrEqualTo(SObjectField field, Date value) { - Object fieldValue = this.getFieldValue(field); - return fieldValue != null && ((Date) fieldValue) >= value; - } - - public Boolean lessThan(SObjectField field, Integer value) { - return this.isLessThan(field, value); - } - - public Boolean lessThan(SObjectField field, Long value) { - return this.isLessThan(field, value); - } - - public Boolean lessThan(SObjectField field, Double value) { - return this.isLessThan(field, value); - } - - public Boolean lessThan(SObjectField field, Decimal value) { - return this.isLessThan(field, value); - } - - public Boolean lessThan(SObjectField field, DateTime value) { - Object fieldValue = this.getFieldValue(field); - return fieldValue != null && ((DateTime) fieldValue) < value; - } - - public Boolean lessThan(SObjectField field, Date value) { - Object fieldValue = this.getFieldValue(field); - return fieldValue != null && ((Date) fieldValue) < value; - } - - public Boolean lessThanOrEqualTo(SObjectField field, Integer value) { - return this.isLessThanOrEqualTo(field, value); - } - - public Boolean lessThanOrEqualTo(SObjectField field, Long value) { - return this.isLessThanOrEqualTo(field, value); - } - - public Boolean lessThanOrEqualTo(SObjectField field, Double value) { - return this.isLessThanOrEqualTo(field, value); - } - - public Boolean lessThanOrEqualTo(SObjectField field, Decimal value) { - return this.isLessThanOrEqualTo(field, value); - } - - public Boolean lessThanOrEqualTo(SObjectField field, DateTime value) { - Object fieldValue = this.getFieldValue(field); - return fieldValue != null && ((DateTime) fieldValue) <= value; - } - - public Boolean lessThanOrEqualTo(SObjectField field, Date value) { - Object fieldValue = this.getFieldValue(field); - return fieldValue != null && ((Date) fieldValue) <= value; - } - - private Boolean isGreaterThan(SObjectField field, Decimal value) { - Decimal fieldValue = this.getFieldValueAsDecimal(field); - return fieldValue != null && fieldValue > value; - } - - private Boolean isGreaterThanOrEqualTo(SObjectField field, Decimal value) { - Decimal fieldValue = this.getFieldValueAsDecimal(field); - return fieldValue != null && fieldValue >= value; - } - - private Boolean isLessThan(SObjectField field, Decimal value) { - Decimal fieldValue = this.getFieldValueAsDecimal(field); - return fieldValue != null && fieldValue < value; - } - - private Boolean isLessThanOrEqualTo(SObjectField field, Decimal value) { - Decimal fieldValue = this.getFieldValueAsDecimal(field); - return fieldValue != null && fieldValue <= value; - } - - private Decimal getFieldValueAsDecimal(SObjectField field) { - Object value = this.getFieldValue(field); - return value == null ? null : Decimal.valueOf(value.toString()); - } - - private Object getFieldValue(SObjectField field) { - return (this.newRecord ?? this.oldRecord).get(field); - } - - private Object getOldFieldValue(SObjectField field) { - return this.oldRecord.get(field); - } - - public Id getId() { - return (this.newRecord ?? this.oldRecord).Id; - } - - public SObject getNewSObject() { - return this.newRecord; - } - - public SObject getOldSObject() { - return this.oldRecord; - } - - public SObject getNewParent(SObjectField lookupField) { - String relationshipName = lookupField.getDescribe().getRelationshipName(); - - this.parentLoader?.load(relationshipName); - - return this.newParents.get(relationshipName); - } - - public SObject getOldParent(SObjectField lookupField) { - return this.getOldParent(lookupField.getDescribe().getRelationshipName()); - } - - public RelatedRecords getRelated(String providerName) { - this.relatedLoader?.load(providerName); - - if (!this.relatedByProviderName.containsKey(providerName)) { - throw new TriggerLibException('No related records provider named ' + providerName + ' is declared. Return it from the queryRelatedOn method of the handler first.'); - } - - return this.relatedByProviderName.get(providerName); - } - - public void put(SObjectField field, Object value) { - this.newRecord.put(field, value); - } - - public void addError(String error) { - (this.newRecord ?? this.oldRecord).addError(error); - } - - public void addError(SObjectField field, String error) { - (this.newRecord ?? this.oldRecord).addError(field, error); - } - - // Internal use only - - public SObject getSObject() { - return this.newRecord ?? this.oldRecord; - } - - public SObject getParent(String relationshipName) { - this.parentLoader?.load(relationshipName); - - return this.newRecord == null ? this.oldParents.get(relationshipName) : this.newParents.get(relationshipName); - } - - public SObject getOldParent(String relationshipName) { - this.parentLoader?.load(relationshipName); - - return this.oldParents.get(relationshipName); - } - - public Id getNewParentId(SObjectField lookupField) { - return this.newRecord?.isSet(lookupField) == true ? (Id) this.newRecord.get(lookupField) : null; - } - - public Id getOldParentId(SObjectField lookupField) { - return this.oldRecord?.isSet(lookupField) == true ? (Id) this.oldRecord.get(lookupField) : null; - } - - public void setNewParent(String relationshipName, SObject parent) { - this.newParents.put(relationshipName, parent); - } - - public void setOldParent(String relationshipName, SObject parent) { - this.oldParents.put(relationshipName, parent); - } - - public void setRelated(Map relatedByProviderName) { - this.relatedByProviderName = relatedByProviderName; - } - - public void setParentLoader(ParentLoader parentLoader) { - this.parentLoader = parentLoader; - } - - public void setRelatedLoader(RelatedLoader relatedLoader) { - this.relatedLoader = relatedLoader; - } - } - - private without sharing class RecordTypesInfo { - private final Map> OBJECT_TYPE_TO_RECORD_TYPE_BY_DEVELOPER_NAME = new Map>(); - - public Id getId(SObjectType objectType, String recordTypeDeveloperName) { - return this.byDeveloperName(objectType, recordTypeDeveloperName)?.getRecordTypeId(); - } - - public Boolean hasRecordTypes(SObjectType objectType) { - if (!this.OBJECT_TYPE_TO_RECORD_TYPE_BY_DEVELOPER_NAME.containsKey(objectType)) { - this.load(objectType); - } - - return this.OBJECT_TYPE_TO_RECORD_TYPE_BY_DEVELOPER_NAME.get(objectType).size() > 1; - } - - public Schema.RecordTypeInfo byDeveloperName(SObjectType objectType, String recordTypeDeveloperName) { - if (!this.OBJECT_TYPE_TO_RECORD_TYPE_BY_DEVELOPER_NAME.containsKey(objectType)) { - this.load(objectType); - } - - return this.OBJECT_TYPE_TO_RECORD_TYPE_BY_DEVELOPER_NAME.get(objectType).get(recordTypeDeveloperName); - } - - private void load(SObjectType objectType) { - Schema.DescribeSObjectResult describe = objectType.getDescribe(SObjectDescribeOptions.DEFERRED); - - this.OBJECT_TYPE_TO_RECORD_TYPE_BY_DEVELOPER_NAME.put(objectType, describe.getRecordTypeInfosByDeveloperName()); - } - } - - public class InsertTriggerRecords implements InsertRecords { - private List records; - - public InsertTriggerRecords(List records) { - this.records = records; - } - - public Set getIds() { - return RecordsAggregator.ids(this.records); - } - - public Set getIdsOf(SObjectField field) { - return RecordsAggregator.idsOf(this.records, field); - } - - public Set getIdsOf(String relationshipName, SObjectField field) { - return RecordsAggregator.idsOf(this.records, relationshipName, field); - } - - public Set getValuesOf(SObjectField field) { - return RecordsAggregator.valuesOf(this.records, field); - } - - public Set getValuesOf(String relationshipName, SObjectField field) { - return RecordsAggregator.valuesOf(this.records, relationshipName, field); - } - - public List getRecords() { - return (List) this.records; - } - - public Integer size() { - return this.records.size(); - } - } - - public class UpdateTriggerRecords implements UpdateRecords { - private List records; - - public UpdateTriggerRecords(List records) { - this.records = records; - } - - public Set getIds() { - return RecordsAggregator.ids(this.records); - } - - public Set getIdsOf(SObjectField field) { - return RecordsAggregator.idsOf(this.records, field); - } - - public Set getIdsOf(String relationshipName, SObjectField field) { - return RecordsAggregator.idsOf(this.records, relationshipName, field); - } - - public Set getOldIdsOf(SObjectField field) { - return RecordsAggregator.oldIdsOf(this.records, field); - } - - public Set getOldIdsOf(String relationshipName, SObjectField field) { - return RecordsAggregator.oldIdsOf(this.records, relationshipName, field); - } - - public Set getValuesOf(SObjectField field) { - return RecordsAggregator.valuesOf(this.records, field); - } - - public Set getValuesOf(String relationshipName, SObjectField field) { - return RecordsAggregator.valuesOf(this.records, relationshipName, field); - } - - public Set getOldValuesOf(SObjectField field) { - return RecordsAggregator.oldValuesOf(this.records, field); - } - - public Set getOldValuesOf(String relationshipName, SObjectField field) { - return RecordsAggregator.oldValuesOf(this.records, relationshipName, field); - } - - public List getRecords() { - return (List) this.records; - } - - public Integer size() { - return this.records.size(); - } - } - - public class DeleteTriggerRecords implements DeleteRecords { - private List records; - - public DeleteTriggerRecords(List records) { - this.records = records; - } - - public Set getIds() { - return RecordsAggregator.ids(this.records); - } - - public Set getIdsOf(SObjectField field) { - return RecordsAggregator.idsOf(this.records, field); - } - - public Set getIdsOf(String relationshipName, SObjectField field) { - return RecordsAggregator.idsOf(this.records, relationshipName, field); - } - - public Set getValuesOf(SObjectField field) { - return RecordsAggregator.valuesOf(this.records, field); - } - - public Set getValuesOf(String relationshipName, SObjectField field) { - return RecordsAggregator.valuesOf(this.records, relationshipName, field); - } - - public List getRecords() { - return (List) this.records; - } - - public Integer size() { - return this.records.size(); - } - } - - public class UndeleteTriggerRecords implements UndeleteRecords { - private List records; - - public UndeleteTriggerRecords(List records) { - this.records = records; - } - - public Set getIds() { - return RecordsAggregator.ids(this.records); - } - - public Set getIdsOf(SObjectField field) { - return RecordsAggregator.idsOf(this.records, field); - } - - public Set getIdsOf(String relationshipName, SObjectField field) { - return RecordsAggregator.idsOf(this.records, relationshipName, field); - } - - public Set getValuesOf(SObjectField field) { - return RecordsAggregator.valuesOf(this.records, field); - } - - public Set getValuesOf(String relationshipName, SObjectField field) { - return RecordsAggregator.valuesOf(this.records, relationshipName, field); - } - - public List getRecords() { - return (List) this.records; - } - - public Integer size() { - return this.records.size(); - } - } - - private class RecordsAggregator { - public Set ids(List records) { - Set ids = new Set(); - - for (TriggerRecord record : records) { - if (record.getId() != null) { - ids.add(record.getId()); - } - } - - return ids; - } - - public Set idsOf(List records, SObjectField field) { - return this.idsFrom(this.rowsOf(records), field); - } - - public Set idsOf(List records, String relationshipName, SObjectField field) { - return this.idsFrom(this.parentsOf(records, relationshipName), field); - } - - public Set oldIdsOf(List records, SObjectField field) { - return this.idsFrom(this.oldRowsOf(records), field); - } - - public Set oldIdsOf(List records, String relationshipName, SObjectField field) { - return this.idsFrom(this.oldParentsOf(records, relationshipName), field); - } - - public Set valuesOf(List records, SObjectField field) { - return this.valuesFrom(this.rowsOf(records), field); - } - - public Set valuesOf(List records, String relationshipName, SObjectField field) { - return this.valuesFrom(this.parentsOf(records, relationshipName), field); - } - - public Set oldValuesOf(List records, SObjectField field) { - return this.valuesFrom(this.oldRowsOf(records), field); - } - - public Set oldValuesOf(List records, String relationshipName, SObjectField field) { - return this.valuesFrom(this.oldParentsOf(records, relationshipName), field); - } - - private List rowsOf(List records) { - List rows = new List(); - - for (TriggerRecord record : records) { - rows.add(record.getSObject()); - } - - return rows; - } - - private List oldRowsOf(List records) { - List rows = new List(); - - for (TriggerRecord record : records) { - rows.add(record.getOldSObject()); - } - - return rows; - } - - private List parentsOf(List records, String relationshipName) { - List parents = new List(); - - for (TriggerRecord record : records) { - parents.add(this.walk(record.getParent(relationshipName.substringBefore('.')), relationshipName)); - } - - return parents; - } - - private List oldParentsOf(List records, String relationshipName) { - List parents = new List(); - - for (TriggerRecord record : records) { - parents.add(this.walk(record.getOldParent(relationshipName.substringBefore('.')), relationshipName)); - } - - return parents; - } - - private SObject walk(SObject parent, String relationshipName) { - List path = relationshipName.split('\\.'); - - for (Integer i = 1; i < path.size() && parent != null; i++) { - parent = parent.getSObject(path[i]); - } - - return parent; - } - - private Set idsFrom(List sources, SObjectField field) { - Set ids = new Set(); - - for (SObject source : sources) { - Object value = source?.get(field); - - if (value != null) { - ids.add((Id) value); - } - } - - return ids; - } - - private Set valuesFrom(List sources, SObjectField field) { - Set values = new Set(); - - for (SObject source : sources) { - Object value = source?.get(field); - - if (value != null) { - values.add(String.valueOf(value)); - } - } - - return values; - } - } - - private class TriggerParentFields implements ParentFields { - private List fields = new List(); - - public ParentFields with(SObjectField field) { - this.fields.add(field.toString()); - return this; - } - - public ParentFields with(SObjectField field1, SObjectField field2) { - this.fields.add(field1.toString()); - this.fields.add(field2.toString()); - return this; - } - - public ParentFields with(SObjectField field1, SObjectField field2, SObjectField field3) { - this.fields.add(field1.toString()); - this.fields.add(field2.toString()); - this.fields.add(field3.toString()); - return this; - } - - public ParentFields with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { - this.fields.add(field1.toString()); - this.fields.add(field2.toString()); - this.fields.add(field3.toString()); - this.fields.add(field4.toString()); - return this; - } - - public ParentFields with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { - this.fields.add(field1.toString()); - this.fields.add(field2.toString()); - this.fields.add(field3.toString()); - this.fields.add(field4.toString()); - this.fields.add(field5.toString()); - return this; - } - - public ParentFields with(Iterable fields) { - for (SObjectField field : fields) { - this.fields.add(field.toString()); - } - return this; - } - - public ParentFields with(String relationshipName, SObjectField field) { - this.fields.add(relationshipName + '.' + field.toString()); - return this; - } - - public ParentFields with(String relationshipName, SObjectField field1, SObjectField field2) { - this.fields.add(relationshipName + '.' + field1.toString()); - this.fields.add(relationshipName + '.' + field2.toString()); - return this; - } - - public ParentFields with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3) { - this.fields.add(relationshipName + '.' + field1.toString()); - this.fields.add(relationshipName + '.' + field2.toString()); - this.fields.add(relationshipName + '.' + field3.toString()); - return this; - } - - public ParentFields with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4) { - this.fields.add(relationshipName + '.' + field1.toString()); - this.fields.add(relationshipName + '.' + field2.toString()); - this.fields.add(relationshipName + '.' + field3.toString()); - this.fields.add(relationshipName + '.' + field4.toString()); - return this; - } - - public ParentFields with(String relationshipName, SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5) { - this.fields.add(relationshipName + '.' + field1.toString()); - this.fields.add(relationshipName + '.' + field2.toString()); - this.fields.add(relationshipName + '.' + field3.toString()); - this.fields.add(relationshipName + '.' + field4.toString()); - this.fields.add(relationshipName + '.' + field5.toString()); - return this; - } - - public ParentFields with(String relationshipName, Iterable fields) { - for (SObjectField field : fields) { - this.fields.add(relationshipName + '.' + field.toString()); - } - return this; - } - - public List getFields() { - return this.fields; - } - } - - public class ProvidedRecords implements RelatedRecords { - private List records = new List(); - private Map> recordsByKey = new Map>(); - - public ProvidedRecords(List rows) { - this.records = rows ?? new List(); - } - - public void groupUnderKey(String key, SObject row) { - if (key == null) { - return; - } - - if (!this.recordsByKey.containsKey(key)) { - this.recordsByKey.put(key, new List()); - } - - this.recordsByKey.get(key).add(row); - } - - public SObject getFirstWhereKeyEquals(Object key) { - List found = this.getAllWhereKeyEquals(key); - return found.isEmpty() ? null : found[0]; - } - - public List getAllWhereKeyEquals(Object key) { - return this.recordsByKey.get(String.valueOf(key)) ?? new List(); - } - - public List getRecords() { - return this.records; - } - - public Boolean isEmpty() { - return this.records.isEmpty(); - } - } - - global class RandomIdGenerator { - private final String RANDOM_STRING_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; - - global Id get(SObjectType objectType) { - return this.get(objectType.getDescribe(SObjectDescribeOptions.DEFERRED).getKeyPrefix()); - } - - global Id get(String prefix) { - String randomPart = ''; - - while (randomPart.length() < 8) { - Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), this.RANDOM_STRING_CHARACTERS.length()); - randomPart += this.RANDOM_STRING_CHARACTERS.substring(idx, idx + 1); - } - - return Id.valueOf(prefix + '0000' + randomPart); - } - } - - global class TriggerLibException extends Exception { - } -} diff --git a/package/main/default/classes/TriggerTypes.cls-meta.xml b/package/main/default/classes/TriggerTypes.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/TriggerTypes.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/classes/TriggerTypesTest.cls b/package/main/default/classes/TriggerTypesTest.cls deleted file mode 100644 index a651e77..0000000 --- a/package/main/default/classes/TriggerTypesTest.cls +++ /dev/null @@ -1,4664 +0,0 @@ -/** - * @description Tests for the TriggerTypes class. - * Copyright (c) 2026 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) - * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/trigger-lib/blob/main/LICENSE) - * - * v0.2.0 - * - * PMD False Positives: - * - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class - * - CognitiveComplexity: It is a library and we tried to put everything into ONE test class - * - NcssTypeCount: It is a library and we tried to put everything into ONE test class - * - ApexUnitTestClassShouldHaveRunAs: The lib does not branch on the running user, so running as one proves nothing - **/ -@SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.NcssTypeCount,PMD.ApexUnitTestClassShouldHaveRunAs') -@IsTest(IsParallel=true testFor='ApexClass:TriggerTypes') -private class TriggerTypesTest { - @IsTest - static void isChangedWhenValueDiffers() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.isChanged(Contact.LastName); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed.'); - } - - @IsTest - static void isChangedWhenValueIsTheSame() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Same'), new Contact(LastName = 'Same')); - - // Test - Boolean result = record.isChanged(Contact.LastName); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed.'); - } - - @IsTest - static void isChangedWhenValueWasSetFromNull() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = 'Manager'), new Contact(LastName = 'New', Title = null)); - - // Test - Boolean result = record.isChanged(Contact.Title); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed.'); - } - - @IsTest - static void isChangedWhenValueWasClearedToNull() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = null), new Contact(LastName = 'New', Title = 'Manager')); - - // Test - Boolean result = record.isChanged(Contact.Title); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed.'); - } - - @IsTest - static void isChangedWhenBothValuesAreNull() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = null), new Contact(LastName = 'New', Title = null)); - - // Test - Boolean result = record.isChanged(Contact.Title); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed.'); - } - - @IsTest - static void isChangedWhenOnlyLetterCaseDiffers() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'DOE'), new Contact(LastName = 'Doe')); - - // Test - Boolean result = record.isChanged(Contact.LastName); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed.'); - } - - @IsTest - static void isChangedToWhenNewValueMatchesAndOldDiffers() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.isChangedTo(Contact.LastName, 'New'); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed to the value.'); - } - - @IsTest - static void isChangedToWhenNewValueDoesNotMatch() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Other'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.isChangedTo(Contact.LastName, 'New'); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed to the value.'); - } - - @IsTest - static void isChangedToWhenOldValueAlreadyMatched() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'New')); - - // Test - Boolean result = record.isChangedTo(Contact.LastName, 'New'); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed to the value.'); - } - - @IsTest - static void isChangedToWhenValueWasSetFromNull() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = 'Manager'), new Contact(LastName = 'New', Title = null)); - - // Test - Boolean result = record.isChangedTo(Contact.Title, 'Manager'); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed to the value.'); - } - - @IsTest - static void isChangedToNullWhenValueWasCleared() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = null), new Contact(LastName = 'New', Title = 'Manager')); - - // Test - Boolean result = record.isChangedTo(Contact.Title, null); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed to the value.'); - } - - @IsTest - static void isChangedToNotNullWhenValueWasSet() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Title = 'Manager'), new Contact(LastName = 'Doe', Title = null)); - - // Test - Boolean result = record.isChangedToNotNull(Contact.Title); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed to a value.'); - } - - @IsTest - static void isChangedToNotNullWhenValueWasReplaced() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Title = 'Director'), new Contact(LastName = 'Doe', Title = 'Manager')); - - // Test - Boolean result = record.isChangedToNotNull(Contact.Title); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed to a value.'); - } - - @IsTest - static void isChangedToNotNullWhenValueWasCleared() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Title = null), new Contact(LastName = 'Doe', Title = 'Manager')); - - // Test - Boolean result = record.isChangedToNotNull(Contact.Title); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed to a value.'); - } - - @IsTest - static void isChangedToNotNullWhenValueIsUnchanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Title = 'Manager'), new Contact(LastName = 'Doe', Title = 'Manager')); - - // Test - Boolean result = record.isChangedToNotNull(Contact.Title); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed to a value.'); - } - - @IsTest - static void isChangedFromWhenOldValueMatchesAndNewDiffers() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.isChangedFrom(Contact.LastName, 'Old'); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed from the value.'); - } - - @IsTest - static void isChangedFromWhenOldValueDoesNotMatch() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Other')); - - // Test - Boolean result = record.isChangedFrom(Contact.LastName, 'Old'); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed from the value.'); - } - - @IsTest - static void isChangedFromWhenNewValueStillMatches() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Old'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.isChangedFrom(Contact.LastName, 'Old'); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed from the value.'); - } - - @IsTest - static void isChangedFromNullWhenValueWasSet() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = 'Manager'), new Contact(LastName = 'New', Title = null)); - - // Test - Boolean result = record.isChangedFrom(Contact.Title, null); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed from the value.'); - } - - @IsTest - static void isChangedFromToWhenBothValuesMatch() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.isChangedFromTo(Contact.LastName, 'Old', 'New'); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed from and to the values.'); - } - - @IsTest - static void isChangedFromToWhenOldValueDoesNotMatch() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Other')); - - // Test - Boolean result = record.isChangedFromTo(Contact.LastName, 'Old', 'New'); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed from and to the values.'); - } - - @IsTest - static void isChangedFromToWhenNewValueDoesNotMatch() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Other'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.isChangedFromTo(Contact.LastName, 'Old', 'New'); - - // Verify - Assert.isFalse(result, 'The field should not be reported as changed from and to the values.'); - } - - @IsTest - static void isChangedFromToWhenFromAndToAreTheSame() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Same'), new Contact(LastName = 'Same')); - - // Test - Boolean result = record.isChangedFromTo(Contact.LastName, 'Same', 'Same'); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed from and to the values.'); - } - - @IsTest - static void isChangedFromToWhenValueWasSetFromNull() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = 'Manager'), new Contact(LastName = 'New', Title = null)); - - // Test - Boolean result = record.isChangedFromTo(Contact.Title, null, 'Manager'); - - // Verify - Assert.isTrue(result, 'The field should be reported as changed from and to the values.'); - } - - @IsTest - static void isAnyChangedWithTwoFieldsWhenOneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Same', FirstName = 'New'), new Contact(LastName = 'Same', FirstName = 'Old')); - - // Test - Boolean result = record.isAnyChanged(Contact.LastName, Contact.FirstName); - - // Verify - Assert.isTrue(result, 'A field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithTwoFieldsWhenNoneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Same', FirstName = 'Same'), new Contact(LastName = 'Same', FirstName = 'Same')); - - // Test - Boolean result = record.isAnyChanged(Contact.LastName, Contact.FirstName); - - // Verify - Assert.isFalse(result, 'No field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithThreeFieldsWhenOneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'new@a.co'), - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'old@a.co') - ); - - // Test - Boolean result = record.isAnyChanged(Contact.LastName, Contact.FirstName, Contact.Email); - - // Verify - Assert.isTrue(result, 'A field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithThreeFieldsWhenNoneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co'), - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co') - ); - - // Test - Boolean result = record.isAnyChanged(Contact.LastName, Contact.FirstName, Contact.Email); - - // Verify - Assert.isFalse(result, 'No field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithFourFieldsWhenOneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co', Title = 'New'), - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co', Title = 'Old') - ); - - // Test - Boolean result = record.isAnyChanged(Contact.LastName, Contact.FirstName, Contact.Email, Contact.Title); - - // Verify - Assert.isTrue(result, 'A field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithFourFieldsWhenNoneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co', Title = 'Same'), - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co', Title = 'Same') - ); - - // Test - Boolean result = record.isAnyChanged(Contact.LastName, Contact.FirstName, Contact.Email, Contact.Title); - - // Verify - Assert.isFalse(result, 'No field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithFiveFieldsWhenLastChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co', Title = 'Same', Description = 'New'), - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co', Title = 'Same', Description = 'Old') - ); - - // Test - Boolean result = record.isAnyChanged(Contact.LastName, Contact.FirstName, Contact.Email, Contact.Title, Contact.Description); - - // Verify - Assert.isTrue(result, 'A field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithFiveFieldsWhenNoneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co', Title = 'Same', Description = 'Same'), - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co', Title = 'Same', Description = 'Same') - ); - - // Test - Boolean result = record.isAnyChanged(Contact.LastName, Contact.FirstName, Contact.Email, Contact.Title, Contact.Description); - - // Verify - Assert.isFalse(result, 'No field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithIterableWhenOneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'new@a.co'), - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'old@a.co') - ); - Iterable fields = new List{ Contact.LastName, Contact.FirstName, Contact.Email }; - - // Test - Boolean result = record.isAnyChanged(fields); - - // Verify - Assert.isTrue(result, 'A field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithIterableWhenNoneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co'), - new Contact(LastName = 'Same', FirstName = 'Same', Email = 'same@a.co') - ); - Iterable fields = new List{ Contact.LastName, Contact.FirstName, Contact.Email }; - - // Test - Boolean result = record.isAnyChanged(fields); - - // Verify - Assert.isFalse(result, 'No field should be reported as changed.'); - } - - @IsTest - static void isAnyChangedWithEmptyIterable() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - Iterable fields = new List(); - - // Test - Boolean result = record.isAnyChanged(fields); - - // Verify - Assert.isFalse(result, 'No field should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithTwoFieldsWhenBothChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'NewFirst'), - new Contact(LastName = 'OldLast', FirstName = 'OldFirst') - ); - - // Test - Boolean result = record.areAllChanged(Contact.LastName, Contact.FirstName); - - // Verify - Assert.isTrue(result, 'All fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithTwoFieldsWhenOnlyOneChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'Same'), - new Contact(LastName = 'OldLast', FirstName = 'Same') - ); - - // Test - Boolean result = record.areAllChanged(Contact.LastName, Contact.FirstName); - - // Verify - Assert.isFalse(result, 'Not all fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithThreeFieldsWhenAllChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'NewFirst', Email = 'new@a.co'), - new Contact(LastName = 'OldLast', FirstName = 'OldFirst', Email = 'old@a.co') - ); - - // Test - Boolean result = record.areAllChanged(Contact.LastName, Contact.FirstName, Contact.Email); - - // Verify - Assert.isTrue(result, 'All fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithThreeFieldsWhenLastDidNotChange() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'NewFirst', Email = 'same@a.co'), - new Contact(LastName = 'OldLast', FirstName = 'OldFirst', Email = 'same@a.co') - ); - - // Test - Boolean result = record.areAllChanged(Contact.LastName, Contact.FirstName, Contact.Email); - - // Verify - Assert.isFalse(result, 'Not all fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithFourFieldsWhenAllChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'NewFirst', Email = 'new@a.co', Title = 'NewTitle'), - new Contact(LastName = 'OldLast', FirstName = 'OldFirst', Email = 'old@a.co', Title = 'OldTitle') - ); - - // Test - Boolean result = record.areAllChanged(Contact.LastName, Contact.FirstName, Contact.Email, Contact.Title); - - // Verify - Assert.isTrue(result, 'All fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithFourFieldsWhenOneDidNotChange() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'NewFirst', Email = 'new@a.co', Title = 'Same'), - new Contact(LastName = 'OldLast', FirstName = 'OldFirst', Email = 'old@a.co', Title = 'Same') - ); - - // Test - Boolean result = record.areAllChanged(Contact.LastName, Contact.FirstName, Contact.Email, Contact.Title); - - // Verify - Assert.isFalse(result, 'Not all fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithFiveFieldsWhenAllChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'NewFirst', Email = 'new@a.co', Title = 'NewTitle', Description = 'NewDescription'), - new Contact(LastName = 'OldLast', FirstName = 'OldFirst', Email = 'old@a.co', Title = 'OldTitle', Description = 'OldDescription') - ); - - // Test - Boolean result = record.areAllChanged(Contact.LastName, Contact.FirstName, Contact.Email, Contact.Title, Contact.Description); - - // Verify - Assert.isTrue(result, 'All fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithFiveFieldsWhenLastDidNotChange() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'NewFirst', Email = 'new@a.co', Title = 'NewTitle', Description = 'Same'), - new Contact(LastName = 'OldLast', FirstName = 'OldFirst', Email = 'old@a.co', Title = 'OldTitle', Description = 'Same') - ); - - // Test - Boolean result = record.areAllChanged(Contact.LastName, Contact.FirstName, Contact.Email, Contact.Title, Contact.Description); - - // Verify - Assert.isFalse(result, 'Not all fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithIterableWhenAllChanged() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'NewFirst', Email = 'new@a.co'), - new Contact(LastName = 'OldLast', FirstName = 'OldFirst', Email = 'old@a.co') - ); - Iterable fields = new List{ Contact.LastName, Contact.FirstName, Contact.Email }; - - // Test - Boolean result = record.areAllChanged(fields); - - // Verify - Assert.isTrue(result, 'All fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithIterableWhenOneDidNotChange() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'NewLast', FirstName = 'Same', Email = 'new@a.co'), - new Contact(LastName = 'OldLast', FirstName = 'Same', Email = 'old@a.co') - ); - Iterable fields = new List{ Contact.LastName, Contact.FirstName, Contact.Email }; - - // Test - Boolean result = record.areAllChanged(fields); - - // Verify - Assert.isFalse(result, 'Not all fields should be reported as changed.'); - } - - @IsTest - static void areAllChangedWithEmptyIterable() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - Iterable fields = new List(); - - // Test - Boolean result = record.areAllChanged(fields); - - // Verify - Assert.isTrue(result, 'All fields should be reported as changed.'); - } - - @IsTest - static void isNullWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isNull(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as null.'); - } - - @IsTest - static void isNullWhenFieldIsEmptyString() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ''), null); - - // Test - Boolean result = record.isNull(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as null.'); - } - - @IsTest - static void isNullWhenFieldIsPopulated() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Populated'), null); - - // Test - Boolean result = record.isNull(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as null.'); - } - - @IsTest - static void isNullWhenNumberFieldIsZero() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Acme', NumberOfEmployees = 0), null); - - // Test - Boolean result = record.isNull(Account.NumberOfEmployees); - - // Verify - Assert.isFalse(result, 'The field should not be reported as null.'); - } - - @IsTest - static void isNullWhenBooleanFieldIsFalse() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal', IsPrivate = false), null); - - // Test - Boolean result = record.isNull(Opportunity.IsPrivate); - - // Verify - Assert.isFalse(result, 'The field should not be reported as null.'); - } - - @IsTest - static void isNullWhenNewValueIsNullAndOldValueIsPopulated() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), new Contact(LastName = 'Doe', Description = 'Old')); - - // Test - Boolean result = record.isNull(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as null.'); - } - - @IsTest - static void isNullOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Deleted')); - - // Test - Boolean result = record.isNull(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as null.'); - } - - @IsTest - static void isNotNullWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isNotNull(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should be reported as null.'); - } - - @IsTest - static void isNotNullWhenFieldIsEmptyString() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ''), null); - - // Test - Boolean result = record.isNotNull(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as not null.'); - } - - @IsTest - static void isNotNullWhenFieldIsPopulated() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Populated'), null); - - // Test - Boolean result = record.isNotNull(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as not null.'); - } - - @IsTest - static void isNotNullWhenNumberFieldIsZero() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Acme', NumberOfEmployees = 0), null); - - // Test - Boolean result = record.isNotNull(Account.NumberOfEmployees); - - // Verify - Assert.isTrue(result, 'The field should be reported as not null.'); - } - - @IsTest - static void isNotNullOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Deleted')); - - // Test - Boolean result = record.isNotNull(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as not null.'); - } - - @IsTest - static void isEmptyWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isEmpty(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as empty.'); - } - - @IsTest - static void isEmptyWhenFieldIsEmptyString() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ''), null); - - // Test - Boolean result = record.isEmpty(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as empty.'); - } - - @IsTest - static void isEmptyWhenFieldIsWhitespace() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ' '), null); - - // Test - Boolean result = record.isEmpty(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as empty.'); - } - - @IsTest - static void isEmptyWhenFieldIsPopulated() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Populated'), null); - - // Test - Boolean result = record.isEmpty(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as empty.'); - } - - @IsTest - static void isEmptyWhenNumberFieldIsZero() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Acme', NumberOfEmployees = 0), null); - - // Test - Boolean result = record.isEmpty(Account.NumberOfEmployees); - - // Verify - Assert.isFalse(result, 'The field should not be reported as empty.'); - } - - @IsTest - static void isEmptyWhenBooleanFieldIsFalse() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal', IsPrivate = false), null); - - // Test - Boolean result = record.isEmpty(Opportunity.IsPrivate); - - // Verify - Assert.isFalse(result, 'The field should not be reported as empty.'); - } - - @IsTest - static void isEmptyWhenNewValueIsEmptyAndOldValueIsPopulated() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ''), new Contact(LastName = 'Doe', Description = 'Old')); - - // Test - Boolean result = record.isEmpty(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as empty.'); - } - - @IsTest - static void isEmptyOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Deleted')); - - // Test - Boolean result = record.isEmpty(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as empty.'); - } - - @IsTest - static void isNotEmptyWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isNotEmpty(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should be reported as empty.'); - } - - @IsTest - static void isNotEmptyWhenFieldIsEmptyString() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ''), null); - - // Test - Boolean result = record.isNotEmpty(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should be reported as empty.'); - } - - @IsTest - static void isNotEmptyWhenFieldIsWhitespace() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ' '), null); - - // Test - Boolean result = record.isNotEmpty(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as not empty.'); - } - - @IsTest - static void isNotEmptyWhenFieldIsPopulated() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Populated'), null); - - // Test - Boolean result = record.isNotEmpty(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as not empty.'); - } - - @IsTest - static void isNotEmptyWhenNumberFieldIsZero() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Acme', NumberOfEmployees = 0), null); - - // Test - Boolean result = record.isNotEmpty(Account.NumberOfEmployees); - - // Verify - Assert.isTrue(result, 'The field should be reported as not empty.'); - } - - @IsTest - static void isNotEmptyOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Deleted')); - - // Test - Boolean result = record.isNotEmpty(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as not empty.'); - } - - @IsTest - static void isBlankWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isBlank(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as blank.'); - } - - @IsTest - static void isBlankWhenFieldIsEmptyString() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ''), null); - - // Test - Boolean result = record.isBlank(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as blank.'); - } - - @IsTest - static void isBlankWhenFieldIsWhitespace() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ' '), null); - - // Test - Boolean result = record.isBlank(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as blank.'); - } - - @IsTest - static void isBlankWhenFieldIsTabAndNewLine() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = '\t\n'), null); - - // Test - Boolean result = record.isBlank(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as blank.'); - } - - @IsTest - static void isBlankWhenFieldIsPopulated() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Populated'), null); - - // Test - Boolean result = record.isBlank(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as blank.'); - } - - @IsTest - static void isBlankWhenFieldHasSurroundingWhitespace() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ' Populated '), null); - - // Test - Boolean result = record.isBlank(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as blank.'); - } - - @IsTest - static void isBlankWhenNumberFieldIsZero() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Acme', NumberOfEmployees = 0), null); - - // Test - Boolean result = record.isBlank(Account.NumberOfEmployees); - - // Verify - Assert.isFalse(result, 'The field should not be reported as blank.'); - } - - @IsTest - static void isBlankWhenBooleanFieldIsFalse() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal', IsPrivate = false), null); - - // Test - Boolean result = record.isBlank(Opportunity.IsPrivate); - - // Verify - Assert.isFalse(result, 'The field should not be reported as blank.'); - } - - @IsTest - static void isBlankWhenNewValueIsWhitespaceAndOldValueIsPopulated() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ' '), new Contact(LastName = 'Doe', Description = 'Old')); - - // Test - Boolean result = record.isBlank(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as blank.'); - } - - @IsTest - static void isBlankWhenNewValueIsPopulatedAndOldValueIsBlank() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Doe', Description = 'Populated'), - new Contact(LastName = 'Doe', Description = ' ') - ); - - // Test - Boolean result = record.isBlank(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as blank.'); - } - - @IsTest - static void isBlankOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Deleted')); - - // Test - Boolean result = record.isBlank(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should not be reported as blank.'); - } - - @IsTest - static void isNotBlankWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isNotBlank(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should be reported as blank.'); - } - - @IsTest - static void isNotBlankWhenFieldIsEmptyString() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ''), null); - - // Test - Boolean result = record.isNotBlank(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should be reported as blank.'); - } - - @IsTest - static void isNotBlankWhenFieldIsWhitespace() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ' '), null); - - // Test - Boolean result = record.isNotBlank(Contact.Description); - - // Verify - Assert.isFalse(result, 'The field should be reported as blank.'); - } - - @IsTest - static void isNotBlankWhenFieldIsPopulated() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Populated'), null); - - // Test - Boolean result = record.isNotBlank(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as not blank.'); - } - - @IsTest - static void isNotBlankWhenNumberFieldIsZero() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Acme', NumberOfEmployees = 0), null); - - // Test - Boolean result = record.isNotBlank(Account.NumberOfEmployees); - - // Verify - Assert.isTrue(result, 'The field should be reported as not blank.'); - } - - @IsTest - static void isNotBlankOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Deleted')); - - // Test - Boolean result = record.isNotBlank(Contact.Description); - - // Verify - Assert.isTrue(result, 'The field should be reported as not blank.'); - } - - @IsTest - static void equalsWhenValueMatches() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.equals(Contact.LastName, 'Doe'); - - // Verify - Assert.isTrue(result, 'The field should equal the value.'); - } - - @IsTest - static void equalsWhenValueDoesNotMatch() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.equals(Contact.LastName, 'Smith'); - - // Verify - Assert.isFalse(result, 'The field should not equal the value.'); - } - - @IsTest - static void equalsWhenOnlyLetterCaseDiffers() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.equals(Contact.LastName, 'DOE'); - - // Verify - Assert.isTrue(result, 'The field should equal the value.'); - } - - @IsTest - static void equalsWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.equals(Contact.Description, 'Anything'); - - // Verify - Assert.isFalse(result, 'The field should not equal the value.'); - } - - @IsTest - static void equalsWhenExpectedValueIsNullAndFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.equals(Contact.Description, null); - - // Verify - Assert.isTrue(result, 'The field should equal the value.'); - } - - @IsTest - static void equalsWhenExpectedValueIsNullAndFieldIsPopulated() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Note'), null); - - // Test - Boolean result = record.equals(Contact.Description, null); - - // Verify - Assert.isFalse(result, 'The field should not equal the value.'); - } - - @IsTest - static void equalsWhenFieldIsEmptyText() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ''), null); - - // Test - Boolean result = record.equals(Contact.Description, ''); - - // Verify - Assert.isTrue(result, 'The field should equal the value.'); - } - - @IsTest - static void equalsWhenFieldIsEmptyTextAndExpectedValueIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = ''), null); - - // Test - Boolean result = record.equals(Contact.Description, null); - - // Verify - Assert.isFalse(result, 'The field should not equal the value.'); - } - - @IsTest - static void equalsOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.equals(Contact.LastName, 'New'); - - // Verify - Assert.isTrue(result, 'The field should equal the value.'); - } - - @IsTest - static void equalsOnUpdateRecordWhenOldValueMatches() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.equals(Contact.LastName, 'Old'); - - // Verify - Assert.isFalse(result, 'The field should not equal the value.'); - } - - @IsTest - static void equalsOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Deleted')); - - // Test - Boolean result = record.equals(Contact.LastName, 'Deleted'); - - // Verify - Assert.isTrue(result, 'The field should equal the value.'); - } - - @IsTest - static void doesNotEqualWhenValueDiffers() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.doesNotEqual(Contact.LastName, 'Smith'); - - // Verify - Assert.isTrue(result, 'The field should not equal the value.'); - } - - @IsTest - static void doesNotEqualWhenValueMatches() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.doesNotEqual(Contact.LastName, 'Doe'); - - // Verify - Assert.isFalse(result, 'The field should equal the value.'); - } - - @IsTest - static void doesNotEqualWhenOnlyLetterCaseDiffers() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.doesNotEqual(Contact.LastName, 'DOE'); - - // Verify - Assert.isFalse(result, 'The field should equal the value.'); - } - - @IsTest - static void doesNotEqualWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.doesNotEqual(Contact.Description, 'Anything'); - - // Verify - Assert.isTrue(result, 'The field should not equal the value.'); - } - - @IsTest - static void doesNotEqualWhenExpectedValueIsNullAndFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.doesNotEqual(Contact.Description, null); - - // Verify - Assert.isFalse(result, 'The field should equal the value.'); - } - - @IsTest - static void doesNotEqualWhenExpectedValueIsNullAndFieldIsPopulated() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Note'), null); - - // Test - Boolean result = record.doesNotEqual(Contact.Description, null); - - // Verify - Assert.isTrue(result, 'The field should not equal the value.'); - } - - @IsTest - static void doesNotEqualOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - - // Test - Boolean result = record.doesNotEqual(Contact.LastName, 'Old'); - - // Verify - Assert.isTrue(result, 'The field should not equal the value.'); - } - - @IsTest - static void doesNotEqualOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Deleted')); - - // Test - Boolean result = record.doesNotEqual(Contact.LastName, 'Deleted'); - - // Verify - Assert.isFalse(result, 'The field should equal the value.'); - } - - @IsTest - static void isInWhenValueIsInSet() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isIn(Contact.LastName, new Set{ 'Doe', 'Roe' }); - - // Verify - Assert.isTrue(result, 'The field should be in the values.'); - } - - @IsTest - static void isInWhenValueIsNotInSet() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isIn(Contact.LastName, new Set{ 'Roe', 'Smith' }); - - // Verify - Assert.isFalse(result, 'The field should not be in the values.'); - } - - @IsTest - static void isInWhenOnlyLetterCaseDiffers() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isIn(Contact.LastName, new Set{ 'DOE' }); - - // Verify - Assert.isTrue(result, 'The field should be in the values.'); - } - - @IsTest - static void isInWithList() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isIn(Contact.LastName, new List{ 'Roe', 'Doe' }); - - // Verify - Assert.isTrue(result, 'The field should be in the values.'); - } - - @IsTest - static void isInWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isIn(Contact.Description, new Set{ 'Anything' }); - - // Verify - Assert.isFalse(result, 'The field should not be in the values.'); - } - - @IsTest - static void isNotInWhenValueIsNotInSet() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isNotIn(Contact.LastName, new Set{ 'Roe', 'Smith' }); - - // Verify - Assert.isTrue(result, 'The field should not be in the values.'); - } - - @IsTest - static void isNotInWhenValueIsInSet() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isNotIn(Contact.LastName, new Set{ 'Doe', 'Roe' }); - - // Verify - Assert.isFalse(result, 'The field should be in the values.'); - } - - @IsTest - static void containsWhenValueIsPresent() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.contains(Contact.Description, 'World'); - - // Verify - Assert.isTrue(result, 'The field should contain the value.'); - } - - @IsTest - static void containsWhenValueIsAbsent() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.contains(Contact.Description, 'Goodbye'); - - // Verify - Assert.isFalse(result, 'The field should not contain the value.'); - } - - @IsTest - static void containsWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.contains(Contact.Description, 'World'); - - // Verify - Assert.isFalse(result, 'The field should not contain the value.'); - } - - @IsTest - static void containsWhenOnlyLetterCaseDiffers() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.contains(Contact.Description, 'world'); - - // Verify - Assert.isFalse(result, 'The field should not contain the value.'); - } - - @IsTest - static void containsWithNumberField() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Acme', NumberOfEmployees = 1500), null); - - // Test - Boolean result = record.contains(Account.NumberOfEmployees, '50'); - - // Verify - Assert.isTrue(result, 'The field should contain the value.'); - } - - @IsTest - static void containsOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Doe', Description = 'New note'), - new Contact(LastName = 'Doe', Description = 'Old note') - ); - - // Test - Boolean result = record.contains(Contact.Description, 'New'); - - // Verify - Assert.isTrue(result, 'The field should contain the value.'); - } - - @IsTest - static void containsOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Old note')); - - // Test - Boolean result = record.contains(Contact.Description, 'Old'); - - // Verify - Assert.isTrue(result, 'The field should contain the value.'); - } - - @IsTest - static void doesNotContainWhenValueIsAbsent() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.doesNotContain(Contact.Description, 'Goodbye'); - - // Verify - Assert.isTrue(result, 'The field should not contain the value.'); - } - - @IsTest - static void doesNotContainWhenValueIsPresent() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.doesNotContain(Contact.Description, 'World'); - - // Verify - Assert.isFalse(result, 'The field should contain the value.'); - } - - @IsTest - static void doesNotContainWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.doesNotContain(Contact.Description, 'World'); - - // Verify - Assert.isTrue(result, 'The field should not contain the value.'); - } - - @IsTest - static void doesNotContainWhenOnlyLetterCaseDiffers() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.doesNotContain(Contact.Description, 'world'); - - // Verify - Assert.isTrue(result, 'The field should not contain the value.'); - } - - @IsTest - static void doesNotContainOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Doe', Description = 'New note'), - new Contact(LastName = 'Doe', Description = 'Old note') - ); - - // Test - Boolean result = record.doesNotContain(Contact.Description, 'Old'); - - // Verify - Assert.isTrue(result, 'The field should not contain the value.'); - } - - @IsTest - static void doesNotContainOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Old note')); - - // Test - Boolean result = record.doesNotContain(Contact.Description, 'Old'); - - // Verify - Assert.isFalse(result, 'The field should contain the value.'); - } - - @IsTest - static void startsWithWhenPrefixMatches() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.startsWith(Contact.Description, 'Hello'); - - // Verify - Assert.isTrue(result, 'The field should start with the value.'); - } - - @IsTest - static void startsWithWhenValueIsNotAtTheStart() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.startsWith(Contact.Description, 'World'); - - // Verify - Assert.isFalse(result, 'The field should not start with the value.'); - } - - @IsTest - static void startsWithWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.startsWith(Contact.Description, 'Hello'); - - // Verify - Assert.isFalse(result, 'The field should not start with the value.'); - } - - @IsTest - static void startsWithWhenOnlyLetterCaseDiffers() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.startsWith(Contact.Description, 'hello'); - - // Verify - Assert.isFalse(result, 'The field should not start with the value.'); - } - - @IsTest - static void startsWithOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Doe', Description = 'New note'), - new Contact(LastName = 'Doe', Description = 'Old note') - ); - - // Test - Boolean result = record.startsWith(Contact.Description, 'New'); - - // Verify - Assert.isTrue(result, 'The field should start with the value.'); - } - - @IsTest - static void startsWithOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Old note')); - - // Test - Boolean result = record.startsWith(Contact.Description, 'Old'); - - // Verify - Assert.isTrue(result, 'The field should start with the value.'); - } - - @IsTest - static void endsWithWhenSuffixMatches() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.endsWith(Contact.Description, 'World'); - - // Verify - Assert.isTrue(result, 'The field should end with the value.'); - } - - @IsTest - static void endsWithWhenValueIsNotAtTheEnd() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.endsWith(Contact.Description, 'Hello'); - - // Verify - Assert.isFalse(result, 'The field should not end with the value.'); - } - - @IsTest - static void endsWithWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.endsWith(Contact.Description, 'World'); - - // Verify - Assert.isFalse(result, 'The field should not end with the value.'); - } - - @IsTest - static void endsWithWhenOnlyLetterCaseDiffers() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe', Description = 'Hello World'), null); - - // Test - Boolean result = record.endsWith(Contact.Description, 'WORLD'); - - // Verify - Assert.isFalse(result, 'The field should not end with the value.'); - } - - @IsTest - static void endsWithOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'Doe', Description = 'Note new'), - new Contact(LastName = 'Doe', Description = 'Note old') - ); - - // Test - Boolean result = record.endsWith(Contact.Description, 'new'); - - // Verify - Assert.isTrue(result, 'The field should end with the value.'); - } - - @IsTest - static void endsWithOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Doe', Description = 'Note old')); - - // Test - Boolean result = record.endsWith(Contact.Description, 'old'); - - // Verify - Assert.isTrue(result, 'The field should end with the value.'); - } - - @IsTest - static void isTrueWhenFieldIsTrue() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal', IsPrivate = true), null); - - // Test - Boolean result = record.isTrue(Opportunity.IsPrivate); - - // Verify - Assert.isTrue(result, 'The field should be reported as true.'); - } - - @IsTest - static void isTrueWhenFieldIsFalse() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal', IsPrivate = false), null); - - // Test - Boolean result = record.isTrue(Opportunity.IsPrivate); - - // Verify - Assert.isFalse(result, 'The field should not be reported as true.'); - } - - @IsTest - static void isTrueWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal'), null); - - // Test - Boolean result = record.isTrue(Opportunity.IsPrivate); - - // Verify - Assert.isFalse(result, 'The field should not be reported as true.'); - } - - @IsTest - static void isTrueWhenFieldIsText() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'true'), null); - - // Test - Boolean result = record.isTrue(Contact.LastName); - - // Verify - Assert.isFalse(result, 'The field should not be reported as true.'); - } - - @IsTest - static void isTrueOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal', IsPrivate = true), new Opportunity(Name = 'Deal', IsPrivate = false)); - - // Test - Boolean result = record.isTrue(Opportunity.IsPrivate); - - // Verify - Assert.isTrue(result, 'The field should be reported as true.'); - } - - @IsTest - static void isTrueOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Opportunity(Name = 'Deal', IsPrivate = true)); - - // Test - Boolean result = record.isTrue(Opportunity.IsPrivate); - - // Verify - Assert.isTrue(result, 'The field should be reported as true.'); - } - - @IsTest - static void isFalseWhenFieldIsFalse() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal', IsPrivate = false), null); - - // Test - Boolean result = record.isFalse(Opportunity.IsPrivate); - - // Verify - Assert.isTrue(result, 'The field should be reported as false.'); - } - - @IsTest - static void isFalseWhenFieldIsTrue() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal', IsPrivate = true), null); - - // Test - Boolean result = record.isFalse(Opportunity.IsPrivate); - - // Verify - Assert.isFalse(result, 'The field should not be reported as false.'); - } - - @IsTest - static void isFalseWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal'), null); - - // Test - Boolean result = record.isFalse(Opportunity.IsPrivate); - - // Verify - Assert.isFalse(result, 'The field should not be reported as false.'); - } - - @IsTest - static void isFalseWhenFieldIsText() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Doe'), null); - - // Test - Boolean result = record.isFalse(Contact.LastName); - - // Verify - Assert.isFalse(result, 'The field should not be reported as false.'); - } - - @IsTest - static void isFalseOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Deal', IsPrivate = false), new Opportunity(Name = 'Deal', IsPrivate = true)); - - // Test - Boolean result = record.isFalse(Opportunity.IsPrivate); - - // Verify - Assert.isTrue(result, 'The field should be reported as false.'); - } - - @IsTest - static void isFalseOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Opportunity(Name = 'Deal', IsPrivate = false)); - - // Test - Boolean result = record.isFalse(Opportunity.IsPrivate); - - // Verify - Assert.isTrue(result, 'The field should be reported as false.'); - } - - @IsTest - static void greaterThanIntegerWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, 50); - - // Verify - Assert.isTrue(result, 'The field should be greater than the value.'); - } - - @IsTest - static void greaterThanIntegerWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, 150); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanIntegerWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, 100); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanIntegerWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, 50); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanOrEqualToIntegerWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.NumberOfEmployees, 50); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToIntegerWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.NumberOfEmployees, 150); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToIntegerWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.NumberOfEmployees, 100); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToIntegerWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.NumberOfEmployees, 50); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void lessThanIntegerWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.lessThan(Account.NumberOfEmployees, 150); - - // Verify - Assert.isTrue(result, 'The field should be less than the value.'); - } - - @IsTest - static void lessThanIntegerWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.lessThan(Account.NumberOfEmployees, 50); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanIntegerWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.lessThan(Account.NumberOfEmployees, 100); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanIntegerWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - - // Test - Boolean result = record.lessThan(Account.NumberOfEmployees, 150); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanOrEqualToIntegerWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.lessThanOrEqualTo(Account.NumberOfEmployees, 150); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToIntegerWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.lessThanOrEqualTo(Account.NumberOfEmployees, 50); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToIntegerWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - - // Test - Boolean result = record.lessThanOrEqualTo(Account.NumberOfEmployees, 100); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToIntegerWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - - // Test - Boolean result = record.lessThanOrEqualTo(Account.NumberOfEmployees, 150); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void greaterThanDoubleWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 2500.25; - - // Test - Boolean result = record.greaterThan(Account.AnnualRevenue, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than the value.'); - } - - @IsTest - static void greaterThanDoubleWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 7500.75; - - // Test - Boolean result = record.greaterThan(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanDoubleWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 5000.5; - - // Test - Boolean result = record.greaterThan(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanDoubleWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - Double value = 2500.25; - - // Test - Boolean result = record.greaterThan(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanOrEqualToDoubleWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 2500.25; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDoubleWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 7500.75; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDoubleWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 5000.5; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDoubleWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - Double value = 2500.25; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void lessThanDoubleWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 7500.75; - - // Test - Boolean result = record.lessThan(Account.AnnualRevenue, value); - - // Verify - Assert.isTrue(result, 'The field should be less than the value.'); - } - - @IsTest - static void lessThanDoubleWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 2500.25; - - // Test - Boolean result = record.lessThan(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanDoubleWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 5000.5; - - // Test - Boolean result = record.lessThan(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanDoubleWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - Double value = 7500.75; - - // Test - Boolean result = record.lessThan(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanOrEqualToDoubleWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 7500.75; - - // Test - Boolean result = record.lessThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDoubleWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 2500.25; - - // Test - Boolean result = record.lessThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDoubleWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 5000.5), null); - Double value = 5000.5; - - // Test - Boolean result = record.lessThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDoubleWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - Double value = 7500.75; - - // Test - Boolean result = record.lessThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void greaterThanDecimalWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 1000.50; - - // Test - Boolean result = record.greaterThan(Opportunity.Amount, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than the value.'); - } - - @IsTest - static void greaterThanDecimalWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 2000.25; - - // Test - Boolean result = record.greaterThan(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanDecimalWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 1500.75; - - // Test - Boolean result = record.greaterThan(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanDecimalWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Test'), null); - Decimal value = 1000.50; - - // Test - Boolean result = record.greaterThan(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanOrEqualToDecimalWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 1000.50; - - // Test - Boolean result = record.greaterThanOrEqualTo(Opportunity.Amount, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDecimalWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 2000.25; - - // Test - Boolean result = record.greaterThanOrEqualTo(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDecimalWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 1500.75; - - // Test - Boolean result = record.greaterThanOrEqualTo(Opportunity.Amount, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDecimalWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Test'), null); - Decimal value = 1000.50; - - // Test - Boolean result = record.greaterThanOrEqualTo(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void lessThanDecimalWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 2000.25; - - // Test - Boolean result = record.lessThan(Opportunity.Amount, value); - - // Verify - Assert.isTrue(result, 'The field should be less than the value.'); - } - - @IsTest - static void lessThanDecimalWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 1000.50; - - // Test - Boolean result = record.lessThan(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanDecimalWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 1500.75; - - // Test - Boolean result = record.lessThan(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanDecimalWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Test'), null); - Decimal value = 2000.25; - - // Test - Boolean result = record.lessThan(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanOrEqualToDecimalWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 2000.25; - - // Test - Boolean result = record.lessThanOrEqualTo(Opportunity.Amount, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDecimalWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 1000.50; - - // Test - Boolean result = record.lessThanOrEqualTo(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDecimalWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Amount = 1500.75), null); - Decimal value = 1500.75; - - // Test - Boolean result = record.lessThanOrEqualTo(Opportunity.Amount, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDecimalWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Opportunity(Name = 'Test'), null); - Decimal value = 2000.25; - - // Test - Boolean result = record.lessThanOrEqualTo(Opportunity.Amount, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void greaterThanDateWhenFieldIsLater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1989, 12, 31); - - // Test - Boolean result = record.greaterThan(Contact.Birthdate, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than the value.'); - } - - @IsTest - static void greaterThanDateWhenFieldIsEarlier() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1990, 1, 2); - - // Test - Boolean result = record.greaterThan(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanDateWhenDatesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1990, 1, 1); - - // Test - Boolean result = record.greaterThan(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanDateWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test'), null); - Date value = Date.newInstance(1989, 12, 31); - - // Test - Boolean result = record.greaterThan(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanOrEqualToDateWhenFieldIsLater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1989, 12, 31); - - // Test - Boolean result = record.greaterThanOrEqualTo(Contact.Birthdate, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDateWhenFieldIsEarlier() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1990, 1, 2); - - // Test - Boolean result = record.greaterThanOrEqualTo(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDateWhenDatesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1990, 1, 1); - - // Test - Boolean result = record.greaterThanOrEqualTo(Contact.Birthdate, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDateWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test'), null); - Date value = Date.newInstance(1989, 12, 31); - - // Test - Boolean result = record.greaterThanOrEqualTo(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void lessThanDateWhenFieldIsEarlier() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1990, 1, 2); - - // Test - Boolean result = record.lessThan(Contact.Birthdate, value); - - // Verify - Assert.isTrue(result, 'The field should be less than the value.'); - } - - @IsTest - static void lessThanDateWhenFieldIsLater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1989, 12, 31); - - // Test - Boolean result = record.lessThan(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanDateWhenDatesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1990, 1, 1); - - // Test - Boolean result = record.lessThan(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanDateWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test'), null); - Date value = Date.newInstance(1990, 1, 2); - - // Test - Boolean result = record.lessThan(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanOrEqualToDateWhenFieldIsEarlier() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1990, 1, 2); - - // Test - Boolean result = record.lessThanOrEqualTo(Contact.Birthdate, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDateWhenFieldIsLater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1989, 12, 31); - - // Test - Boolean result = record.lessThanOrEqualTo(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDateWhenDatesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', Birthdate = Date.newInstance(1990, 1, 1)), null); - Date value = Date.newInstance(1990, 1, 1); - - // Test - Boolean result = record.lessThanOrEqualTo(Contact.Birthdate, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDateWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test'), null); - Date value = Date.newInstance(1990, 1, 2); - - // Test - Boolean result = record.lessThanOrEqualTo(Contact.Birthdate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void greaterThanDateTimeWhenFieldIsLater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 11, 0, 0); - - // Test - Boolean result = record.greaterThan(Contact.EmailBouncedDate, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than the value.'); - } - - @IsTest - static void greaterThanDateTimeWhenFieldIsEarlier() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 13, 0, 0); - - // Test - Boolean result = record.greaterThan(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanDateTimeWhenDateTimesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0); - - // Test - Boolean result = record.greaterThan(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanDateTimeWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test'), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 11, 0, 0); - - // Test - Boolean result = record.greaterThan(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanOrEqualToDateTimeWhenFieldIsLater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 11, 0, 0); - - // Test - Boolean result = record.greaterThanOrEqualTo(Contact.EmailBouncedDate, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDateTimeWhenFieldIsEarlier() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 13, 0, 0); - - // Test - Boolean result = record.greaterThanOrEqualTo(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDateTimeWhenDateTimesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0); - - // Test - Boolean result = record.greaterThanOrEqualTo(Contact.EmailBouncedDate, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToDateTimeWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test'), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 11, 0, 0); - - // Test - Boolean result = record.greaterThanOrEqualTo(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void lessThanDateTimeWhenFieldIsEarlier() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 13, 0, 0); - - // Test - Boolean result = record.lessThan(Contact.EmailBouncedDate, value); - - // Verify - Assert.isTrue(result, 'The field should be less than the value.'); - } - - @IsTest - static void lessThanDateTimeWhenFieldIsLater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 11, 0, 0); - - // Test - Boolean result = record.lessThan(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanDateTimeWhenDateTimesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0); - - // Test - Boolean result = record.lessThan(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanDateTimeWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test'), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 13, 0, 0); - - // Test - Boolean result = record.lessThan(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanOrEqualToDateTimeWhenFieldIsEarlier() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 13, 0, 0); - - // Test - Boolean result = record.lessThanOrEqualTo(Contact.EmailBouncedDate, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDateTimeWhenFieldIsLater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 11, 0, 0); - - // Test - Boolean result = record.lessThanOrEqualTo(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDateTimeWhenDateTimesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test', EmailBouncedDate = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0)), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 12, 0, 0); - - // Test - Boolean result = record.lessThanOrEqualTo(Contact.EmailBouncedDate, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToDateTimeWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'Test'), null); - DateTime value = DateTime.newInstanceGmt(2024, 1, 1, 13, 0, 0); - - // Test - Boolean result = record.lessThanOrEqualTo(Contact.EmailBouncedDate, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void greaterThanLongWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 50L; - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than the value.'); - } - - @IsTest - static void greaterThanLongWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 150L; - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanLongWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 100L; - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanLongWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - Long value = 50L; - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanOrEqualToLongWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 50L; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.NumberOfEmployees, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToLongWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 150L; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToLongWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 100L; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.NumberOfEmployees, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToLongWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - Long value = 50L; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanLongBeyondDoublePrecisionWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 9007199254740993L), null); - Long value = 9007199254740992L; - - // Test - Boolean result = record.greaterThan(Account.AnnualRevenue, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than the value.'); - } - - @IsTest - static void greaterThanLongBeyondDoublePrecisionWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 9007199254740993L), null); - Long value = 9007199254740993L; - - // Test - Boolean result = record.greaterThan(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than the value.'); - } - - @IsTest - static void greaterThanOrEqualToLongBeyondDoublePrecisionWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 9007199254740992L), null); - Long value = 9007199254740993L; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isFalse(result, 'The field should not be greater than or equal to the value.'); - } - - @IsTest - static void greaterThanOrEqualToLongBeyondDoublePrecisionWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(AnnualRevenue = 9007199254740993L), null); - Long value = 9007199254740993L; - - // Test - Boolean result = record.greaterThanOrEqualTo(Account.AnnualRevenue, value); - - // Verify - Assert.isTrue(result, 'The field should be greater than or equal to the value.'); - } - - @IsTest - static void lessThanLongWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 150L; - - // Test - Boolean result = record.lessThan(Account.NumberOfEmployees, value); - - // Verify - Assert.isTrue(result, 'The field should be less than the value.'); - } - - @IsTest - static void lessThanLongWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 50L; - - // Test - Boolean result = record.lessThan(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanLongWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 100L; - - // Test - Boolean result = record.lessThan(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanLongWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - Long value = 150L; - - // Test - Boolean result = record.lessThan(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than the value.'); - } - - @IsTest - static void lessThanOrEqualToLongWhenFieldIsLower() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 150L; - - // Test - Boolean result = record.lessThanOrEqualTo(Account.NumberOfEmployees, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToLongWhenFieldIsGreater() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 50L; - - // Test - Boolean result = record.lessThanOrEqualTo(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToLongWhenValuesAreEqual() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), null); - Long value = 100L; - - // Test - Boolean result = record.lessThanOrEqualTo(Account.NumberOfEmployees, value); - - // Verify - Assert.isTrue(result, 'The field should be less than or equal to the value.'); - } - - @IsTest - static void lessThanOrEqualToLongWhenFieldIsNull() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Account(Name = 'Test'), null); - Long value = 150L; - - // Test - Boolean result = record.lessThanOrEqualTo(Account.NumberOfEmployees, value); - - // Verify - Assert.isFalse(result, 'The field should not be less than or equal to the value.'); - } - - @IsTest - static void greaterThanOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Account(NumberOfEmployees = 100), new Account(NumberOfEmployees = 10)); - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, 50); - - // Verify - Assert.isTrue(result, 'The comparison should read the new record.'); - } - - @IsTest - static void greaterThanOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Account(NumberOfEmployees = 100)); - - // Test - Boolean result = record.greaterThan(Account.NumberOfEmployees, 50); - - // Verify - Assert.isTrue(result, 'The comparison should read the old record.'); - } - - @IsTest - static void isRecordTypeWhenSObjectHasNoRecordTypes() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new User(), null); - Exception expectedException = null; - - // Test - try { - record.isRecordType('Standard'); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('User has no record types, so isRecordType cannot be used on it.', expectedException.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void isNotRecordTypeWhenSObjectHasNoRecordTypes() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new User(), null); - Exception expectedException = null; - - // Test - try { - record.isNotRecordType('Standard'); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'User has no record types, so isNotRecordType cannot be used on it.', - expectedException.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void isRecordTypeOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new User(LastName = 'Old')); - Exception expectedException = null; - - // Test - try { - record.isRecordType('Standard'); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('User has no record types, so isRecordType cannot be used on it.', expectedException.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void isNotRecordTypeOnDeleteRecord() { - // Setup - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new User(LastName = 'Old')); - Exception expectedException = null; - - // Test - try { - record.isNotRecordType('Standard'); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'User has no record types, so isNotRecordType cannot be used on it.', - expectedException.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void isRecordTypeWhenGroupHasNoRecordTypes() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Group(), null); - Exception expectedException = null; - - // Test - try { - record.isRecordType('Standard'); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual('Group has no record types, so isRecordType cannot be used on it.', expectedException.getMessage(), 'The exception message should match the expected one.'); - } - - @IsTest - static void getNewSObjectOnInsertRecord() { - // Setup - Contact newContact = new Contact(LastName = 'New'); - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(newContact, null); - - // Test - SObject result = record.getNewSObject(); - - // Verify - Assert.areEqual(newContact, result, 'The new record should be returned.'); - } - - @IsTest - static void getOldSObjectOnInsertRecord() { - // Setup - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), null); - - // Test - SObject result = record.getOldSObject(); - - // Verify - Assert.isNull(result, 'The old record should be null.'); - } - - @IsTest - static void getNewSObjectOnUpdateRecord() { - // Setup - Contact newContact = new Contact(LastName = 'New'); - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(newContact, new Contact(LastName = 'Old')); - - // Test - SObject result = record.getNewSObject(); - - // Verify - Assert.areEqual(newContact, result, 'The new record should be returned.'); - } - - @IsTest - static void getOldSObjectOnUpdateRecord() { - // Setup - Contact oldContact = new Contact(LastName = 'Old'); - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), oldContact); - - // Test - SObject result = record.getOldSObject(); - - // Verify - Assert.areEqual(oldContact, result, 'The old record should be returned.'); - } - - @IsTest - static void getNewSObjectOnDeleteRecord() { - // Setup - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Old')); - - // Test - SObject result = record.getNewSObject(); - - // Verify - Assert.isNull(result, 'The new record should be null.'); - } - - @IsTest - static void getOldSObjectOnDeleteRecord() { - // Setup - Id contactId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - Contact oldContact = new Contact(Id = contactId, LastName = 'Old'); - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, oldContact); - - // Test - SObject result = record.getOldSObject(); - - // Verify - Assert.areEqual(oldContact, result, 'The old record should be returned.'); - } - - @IsTest - static void getNewSObjectWhenSourceRecordChanges() { - // Setup - Contact newContact = new Contact(LastName = 'New'); - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(newContact, null); - newContact.Title = 'Manager'; - - // Test - SObject result = record.getNewSObject(); - - // Verify - Assert.areEqual('Manager', result.get(Contact.Title), 'The change on the source record should be visible.'); - } - - @IsTest - static void getOldSObjectWhenSourceRecordChanges() { - // Setup - Contact oldContact = new Contact(LastName = 'Old'); - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), oldContact); - oldContact.Title = 'Manager'; - - // Test - SObject result = record.getOldSObject(); - - // Verify - Assert.areEqual('Manager', result.get(Contact.Title), 'The change on the source record should be visible.'); - } - - @IsTest - static void getIdOnDeleteRecord() { - // Setup - Id contactId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - TriggerTypes.DeleteRecord record = new TriggerTypes.TriggerRecord(null, new Contact(Id = contactId, LastName = 'Old')); - - // Test - Id result = record.getId(); - - // Verify - Assert.areEqual(contactId, result, 'The old record Id should be returned.'); - } - - @IsTest - static void getIdWhenNewAndOldIdsDiffer() { - // Setup - Id firstContactId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - Id secondContactId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(Id = firstContactId, LastName = 'New'), new Contact(Id = secondContactId, LastName = 'Old')); - - // Test - Id result = record.getId(); - - // Verify - Assert.areEqual(firstContactId, result, 'The new record Id should be returned.'); - } - - @IsTest - static void equalsAfterPut() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), null); - record.put(Contact.Title, 'Manager'); - - // Test - Boolean result = record.equals(Contact.Title, 'Manager'); - - // Verify - Assert.isTrue(result, 'The field should equal the put value.'); - } - - @IsTest - static void put() { - // Setup - Contact newContact = new Contact(LastName = 'New'); - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(newContact, null); - - // Test - record.put(Contact.Title, 'Manager'); - - // Verify - Assert.areEqual('Manager', newContact.Title, 'The field should be set on the source record.'); - } - - @IsTest - static void putWhenFieldHasValue() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = 'Analyst'), null); - - // Test - record.put(Contact.Title, 'Manager'); - - // Verify - Assert.areEqual('Manager', record.getNewSObject().get(Contact.Title), 'The field should be overwritten.'); - } - - @IsTest - static void putWithNullValue() { - // Setup - TriggerTypes.InsertRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = 'Analyst'), null); - - // Test - record.put(Contact.Title, null); - - // Verify - Assert.isNull(record.getNewSObject().get(Contact.Title), 'The field should be cleared.'); - } - - @IsTest - static void putOnUpdateRecord() { - // Setup - TriggerTypes.UpdateRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', Title = 'Analyst'), new Contact(LastName = 'Old', Title = 'Analyst')); - - // Test - record.put(Contact.Title, 'Manager'); - - // Verify - Assert.areEqual('Analyst', record.getOldSObject().get(Contact.Title), 'The old record should not change.'); - } - - @IsTest - static void addErrorOnRejectableInsertRecord() { - // Setup - Contact newContact = new Contact(LastName = 'New'); - TriggerTypes.RejectableInsertRecord record = new TriggerTypes.TriggerRecord(newContact, null); - - // Test - record.addError('The record is not allowed.'); - - // Verify - Assert.areEqual('The record is not allowed.', newContact.getErrors()[0].getMessage(), 'The error should be attached to the record.'); - } - - @IsTest - static void addErrorWithFieldOnRejectableInsertRecord() { - // Setup - Contact newContact = new Contact(LastName = 'New'); - TriggerTypes.RejectableInsertRecord record = new TriggerTypes.TriggerRecord(newContact, null); - - // Test - record.addError(Contact.Email, 'The email is not allowed.'); - - // Verify - Assert.areEqual(new List{ 'Email' }, newContact.getErrors()[0].getFields(), 'The error should be attached to the field.'); - } - - @IsTest - static void addErrorOnRejectableUpdateRecord() { - // Setup - Contact newContact = new Contact(LastName = 'New'); - TriggerTypes.RejectableUpdateRecord record = new TriggerTypes.TriggerRecord(newContact, new Contact(LastName = 'Old')); - - // Test - record.addError('The record is not allowed.'); - - // Verify - Assert.areEqual('The record is not allowed.', newContact.getErrors()[0].getMessage(), 'The error should be attached to the new record.'); - } - - @IsTest - static void addErrorWithFieldOnRejectableUpdateRecord() { - // Setup - Contact newContact = new Contact(LastName = 'New'); - TriggerTypes.RejectableUpdateRecord record = new TriggerTypes.TriggerRecord(newContact, new Contact(LastName = 'Old')); - - // Test - record.addError(Contact.Email, 'The email is not allowed.'); - - // Verify - Assert.areEqual(new List{ 'Email' }, newContact.getErrors()[0].getFields(), 'The error should be attached to the field.'); - } - - @IsTest - static void addErrorOnRejectableDeleteRecord() { - // Setup - Contact oldContact = new Contact(LastName = 'Old'); - TriggerTypes.RejectableDeleteRecord record = new TriggerTypes.TriggerRecord(null, oldContact); - - // Test - record.addError('The record is not allowed.'); - - // Verify - Assert.areEqual('The record is not allowed.', oldContact.getErrors()[0].getMessage(), 'The error should be attached to the old record.'); - } - - @IsTest - static void addErrorWithFieldOnRejectableDeleteRecord() { - // Setup - Contact oldContact = new Contact(LastName = 'Old'); - TriggerTypes.RejectableDeleteRecord record = new TriggerTypes.TriggerRecord(null, oldContact); - - // Test - record.addError(Contact.Email, 'The email is not allowed.'); - - // Verify - Assert.areEqual(new List{ 'Email' }, oldContact.getErrors()[0].getFields(), 'The error should be attached to the field.'); - } - - @IsTest - static void getNewParent() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Account parentAccount = new Account(Id = accountId, Name = 'Parent'); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', AccountId = accountId), null); - record.setNewParent('Account', parentAccount); - - // Test - SObject result = record.getNewParent(Contact.AccountId); - - // Verify - Assert.areEqual(parentAccount, result, 'The enriched parent should be returned.'); - } - - @IsTest - static void getOldParent() { - // Setup - Id firstAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Id secondAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Account priorParentAccount = new Account(Id = firstAccountId, Name = 'Prior Parent'); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'New', AccountId = secondAccountId), - new Contact(LastName = 'Old', AccountId = firstAccountId) - ); - record.setOldParent('Account', priorParentAccount); - - // Test - SObject result = record.getOldParent(Contact.AccountId); - - // Verify - Assert.areEqual(priorParentAccount, result, 'The enriched parent should be returned.'); - } - - @IsTest - static void getNewParentWithParentLoader() { - // Setup - TestParentLoader parentLoader = new TestParentLoader(); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), null); - record.setParentLoader(parentLoader); - - // Test - record.getNewParent(Contact.AccountId); - - // Verify - Assert.areEqual(new List{ 'Account' }, parentLoader.loadedRelationshipNames, 'The parent should be loaded before it is read.'); - } - - @IsTest - static void getOldParentWithParentLoader() { - // Setup - TestParentLoader parentLoader = new TestParentLoader(); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), new Contact(LastName = 'Old')); - record.setParentLoader(parentLoader); - - // Test - record.getOldParent(Contact.AccountId); - - // Verify - Assert.areEqual(new List{ 'Account' }, parentLoader.loadedRelationshipNames, 'The parent should be loaded before it is read.'); - } - - @IsTest - static void getOldParentWhenBothSidesAreEnriched() { - // Setup - Id firstAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Id secondAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'New', AccountId = firstAccountId), - new Contact(LastName = 'Old', AccountId = secondAccountId) - ); - record.setOldParent('Account', new Account(Id = secondAccountId, Name = 'Prior Parent')); - record.setNewParent('Account', new Account(Id = firstAccountId, Name = 'Current Parent')); - - // Test - SObject result = record.getOldParent(Contact.AccountId); - - // Verify - Assert.areEqual('Prior Parent', result.get(Account.Name), 'The old parent should be returned.'); - } - - @IsTest - static void getNewParentWhenBothSidesAreEnriched() { - // Setup - Id firstAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Id secondAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'New', AccountId = firstAccountId), - new Contact(LastName = 'Old', AccountId = secondAccountId) - ); - record.setNewParent('Account', new Account(Id = firstAccountId, Name = 'Current Parent')); - record.setOldParent('Account', new Account(Id = secondAccountId, Name = 'Prior Parent')); - - // Test - SObject result = record.getNewParent(Contact.AccountId); - - // Verify - Assert.areEqual('Current Parent', result.get(Account.Name), 'The new parent should be returned.'); - } - - @IsTest - static void getNewParentWhenLookupIsNotEnriched() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', AccountId = accountId), null); - record.setNewParent('Account', new Account(Id = accountId, Name = 'Parent')); - - // Test - SObject result = record.getNewParent(Contact.ReportsToId); - - // Verify - Assert.isNull(result, 'The parent should be null.'); - } - - @IsTest - static void getOldParentWhenLookupIsNotEnriched() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'New', AccountId = accountId), - new Contact(LastName = 'Old', AccountId = accountId) - ); - record.setOldParent('Account', new Account(Id = accountId, Name = 'Parent')); - - // Test - SObject result = record.getOldParent(Contact.ReportsToId); - - // Verify - Assert.isNull(result, 'The parent should be null.'); - } - - @IsTest - static void getOldParentWhenOnlyNewSideIsEnriched() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'New', AccountId = accountId), - new Contact(LastName = 'Old', AccountId = accountId) - ); - record.setNewParent('Account', new Account(Id = accountId, Name = 'Parent')); - - // Test - SObject result = record.getOldParent(Contact.AccountId); - - // Verify - Assert.isNull(result, 'The parent should be null.'); - } - - @IsTest - static void getNewParentWhenOnlyOldSideIsEnriched() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord( - new Contact(LastName = 'New', AccountId = accountId), - new Contact(LastName = 'Old', AccountId = accountId) - ); - record.setOldParent('Account', new Account(Id = accountId, Name = 'Parent')); - - // Test - SObject result = record.getNewParent(Contact.AccountId); - - // Verify - Assert.isNull(result, 'The parent should be null.'); - } - - @IsTest - static void getNewParentWhenOtherLookupPointsToSameObject() { - // Setup - Id managerId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', ReportsToId = managerId), null); - record.setNewParent('ReportsTo', new Contact(Id = managerId, LastName = 'Manager')); - - // Test - SObject result = record.getNewParent(Contact.MasterRecordId); - - // Verify - Assert.isNull(result, 'The parent should be null.'); - } - - @IsTest - static void enrichNewWhenRelationshipIsAlreadyEnriched() { - // Setup - Id firstAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Id secondAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', AccountId = firstAccountId), null); - record.setNewParent('Account', new Account(Id = firstAccountId, Name = 'Original Parent')); - record.setNewParent('Account', new Account(Id = secondAccountId, Name = 'Re-pointed Parent')); - - // Test - SObject result = record.getNewParent(Contact.AccountId); - - // Verify - Assert.areEqual('Re-pointed Parent', result.get(Account.Name), 'The parent should be replaced.'); - } - - @IsTest - static void enrichNewWithTwoRelationships() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Id contactId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New', AccountId = accountId), null); - record.setNewParent('Account', new Account(Id = accountId, Name = 'Parent')); - record.setNewParent('ReportsTo', new Contact(Id = contactId, LastName = 'Manager')); - - // Test - SObject result = record.getNewParent(Contact.AccountId); - - // Verify - Assert.areEqual('Parent', result.get(Account.Name), 'The first parent should be kept.'); - } - - @IsTest - static void withOneField() { - // Test - List fields = TriggerTypes.ParentFields.with(Account.Name).getFields(); - - // Verify - Assert.areEqual(new List{ 'Name' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withTwoFields() { - // Test - List fields = TriggerTypes.ParentFields.with(Account.Name, Account.Industry).getFields(); - - // Verify - Assert.areEqual(new List{ 'Name', 'Industry' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withThreeFields() { - // Test - List fields = TriggerTypes.ParentFields.with(Account.Name, Account.Industry, Account.Description).getFields(); - - // Verify - Assert.areEqual(new List{ 'Name', 'Industry', 'Description' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withFourFields() { - // Test - List fields = TriggerTypes.ParentFields.with(Account.Name, Account.Industry, Account.Description, Account.Website).getFields(); - - // Verify - Assert.areEqual(new List{ 'Name', 'Industry', 'Description', 'Website' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withFiveFields() { - // Test - List fields = TriggerTypes.ParentFields.with(Account.Name, Account.Industry, Account.Description, Account.Website, Account.NumberOfEmployees).getFields(); - - // Verify - Assert.areEqual(new List{ 'Name', 'Industry', 'Description', 'Website', 'NumberOfEmployees' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withIterableOfFields() { - // Setup - List fieldsToSelect = new List{ - Account.Name, - Account.Industry, - Account.Description, - Account.Website, - Account.NumberOfEmployees, - Account.AnnualRevenue - }; - - // Test - List fields = TriggerTypes.ParentFields.with(fieldsToSelect).getFields(); - - // Verify - Assert.areEqual( - new List{ 'Name', 'Industry', 'Description', 'Website', 'NumberOfEmployees', 'AnnualRevenue' }, - fields, - 'The selected fields should match the expected ones.' - ); - } - - @IsTest - static void withRelationshipAndOneField() { - // Test - List fields = TriggerTypes.ParentFields.with('Parent', Account.Name).getFields(); - - // Verify - Assert.areEqual(new List{ 'Parent.Name' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withRelationshipAndTwoFields() { - // Test - List fields = TriggerTypes.ParentFields.with('Parent', Account.Name, Account.Industry).getFields(); - - // Verify - Assert.areEqual(new List{ 'Parent.Name', 'Parent.Industry' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withRelationshipAndThreeFields() { - // Test - List fields = TriggerTypes.ParentFields.with('Parent', Account.Name, Account.Industry, Account.Description).getFields(); - - // Verify - Assert.areEqual(new List{ 'Parent.Name', 'Parent.Industry', 'Parent.Description' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withRelationshipAndFourFields() { - // Test - List fields = TriggerTypes.ParentFields.with('Parent', Account.Name, Account.Industry, Account.Description, Account.Website).getFields(); - - // Verify - Assert.areEqual( - new List{ 'Parent.Name', 'Parent.Industry', 'Parent.Description', 'Parent.Website' }, - fields, - 'The selected fields should match the expected ones.' - ); - } - - @IsTest - static void withRelationshipAndFiveFields() { - // Test - List fields = TriggerTypes.ParentFields.with('Parent', Account.Name, Account.Industry, Account.Description, Account.Website, Account.NumberOfEmployees).getFields(); - - // Verify - Assert.areEqual( - new List{ 'Parent.Name', 'Parent.Industry', 'Parent.Description', 'Parent.Website', 'Parent.NumberOfEmployees' }, - fields, - 'The selected fields should match the expected ones.' - ); - } - - @IsTest - static void withRelationshipAndIterableOfFields() { - // Setup - List fieldsToSelect = new List{ - Account.Name, - Account.Industry, - Account.Description, - Account.Website, - Account.NumberOfEmployees, - Account.AnnualRevenue - }; - - // Test - List fields = TriggerTypes.ParentFields.with('Parent', fieldsToSelect).getFields(); - - // Verify - Assert.areEqual( - new List{ 'Parent.Name', 'Parent.Industry', 'Parent.Description', 'Parent.Website', 'Parent.NumberOfEmployees', 'Parent.AnnualRevenue' }, - fields, - 'The selected fields should match the expected ones.' - ); - } - - @IsTest - static void withPlainAndRelationshipFieldsChained() { - // Test - List fields = TriggerTypes.ParentFields.with(Account.Name, Account.Industry).with('Parent', Account.Name).with(Account.Website).getFields(); - - // Verify - Assert.areEqual(new List{ 'Name', 'Industry', 'Parent.Name', 'Website' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withDifferentRelationshipsChained() { - // Test - List fields = TriggerTypes.ParentFields.with('Account', Account.Name).with('ReportsTo', Contact.Email).with('Account', Account.Industry).getFields(); - - // Verify - Assert.areEqual(new List{ 'Account.Name', 'ReportsTo.Email', 'Account.Industry' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void withChaining() { - // Setup - TriggerTypes.ParentFields selection = TriggerTypes.ParentFields.with(Account.Name); - - // Test - TriggerTypes.ParentFields chainedSelection = selection.with(Account.Industry); - - // Verify - Assert.isTrue(chainedSelection === selection, 'The same selection should be returned.'); - } - - @IsTest - static void withRelationshipChaining() { - // Setup - TriggerTypes.ParentFields selection = TriggerTypes.ParentFields.with(Account.Name); - - // Test - TriggerTypes.ParentFields chainedSelection = selection.with('Parent', Account.Industry); - - // Verify - Assert.isTrue(chainedSelection === selection, 'The same selection should be returned.'); - } - - @IsTest - static void withInSeparateStatements() { - // Setup - TriggerTypes.ParentFields selection = TriggerTypes.ParentFields; - - selection.with(Account.Name); - selection.with('Parent', Account.Industry); - - // Test - List fields = selection.with(new List{ Account.Website }).getFields(); - - // Verify - Assert.areEqual(new List{ 'Name', 'Parent.Industry', 'Website' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void parentFieldsWhenReadTwice() { - // Setup - TriggerTypes.ParentFields.with(Account.Name, Account.Industry); - - // Test - List fields = TriggerTypes.ParentFields.getFields(); - - // Verify - Assert.areEqual(new List(), fields, 'The selected fields should be empty.'); - } - - @IsTest - static void getFieldsWithRepeatedField() { - // Test - List fields = TriggerTypes.ParentFields.with(Account.Name).with(Account.Industry).with(Account.Name).getFields(); - - // Verify - Assert.areEqual(new List{ 'Name', 'Industry', 'Name' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void getFieldsWithRepeatedFieldInSingleCall() { - // Test - List fields = TriggerTypes.ParentFields.with(Account.Name, Account.Name, Account.Name).getFields(); - - // Verify - Assert.areEqual(new List{ 'Name', 'Name', 'Name' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void getFieldsWithRepeatedRelationshipField() { - // Test - List fields = TriggerTypes.ParentFields.with('Parent', Account.Name).with('Parent', Account.Name).getFields(); - - // Verify - Assert.areEqual(new List{ 'Parent.Name', 'Parent.Name' }, fields, 'The selected fields should match the expected ones.'); - } - - @IsTest - static void providedRecordsGetFirstWhereKeyEquals() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Opportunity firstOpportunity = new Opportunity(Name = 'First', AccountId = accountId); - Opportunity secondOpportunity = new Opportunity(Name = 'Second', AccountId = accountId); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(new List{ firstOpportunity, secondOpportunity }); - related.groupUnderKey(accountId, firstOpportunity); - related.groupUnderKey(accountId, secondOpportunity); - - // Test - SObject result = related.getFirstWhereKeyEquals(accountId); - - // Verify - Assert.areEqual(firstOpportunity, result, 'The first grouped record should be returned.'); - } - - @IsTest - static void providedRecordsGetAllWhereKeyEquals() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Opportunity firstOpportunity = new Opportunity(Name = 'First', AccountId = accountId); - Opportunity secondOpportunity = new Opportunity(Name = 'Second', AccountId = accountId); - Opportunity otherOpportunity = new Opportunity(Name = 'Other', AccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType)); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(new List{ firstOpportunity, secondOpportunity, otherOpportunity }); - related.groupUnderKey(accountId, firstOpportunity); - related.groupUnderKey(accountId, secondOpportunity); - related.groupUnderKey(otherOpportunity.AccountId, otherOpportunity); - - // Test - List result = related.getAllWhereKeyEquals(accountId); - - // Verify - Assert.areEqual(new List{ firstOpportunity, secondOpportunity }, result, 'The grouped records should be returned.'); - } - - @IsTest - static void providedRecordsGetAllWhereKeyEqualsWhenKeyIsUnknown() { - // Setup - Opportunity deal = new Opportunity(Name = 'First', AccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType)); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(new List{ deal }); - related.groupUnderKey(deal.AccountId, deal); - - // Test - List result = related.getAllWhereKeyEquals(new TriggerTypes.RandomIdGenerator().get(Account.SObjectType)); - - // Verify - Assert.areEqual(new List(), result, 'An empty list should be returned.'); - } - - @IsTest - static void providedRecordsGetFirstWhereKeyEqualsWhenKeyIsUnknown() { - // Setup - Opportunity deal = new Opportunity(Name = 'First', AccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType)); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(new List{ deal }); - related.groupUnderKey(deal.AccountId, deal); - - // Test - SObject result = related.getFirstWhereKeyEquals(new TriggerTypes.RandomIdGenerator().get(Account.SObjectType)); - - // Verify - Assert.isNull(result, 'The record should be null.'); - } - - @IsTest - static void providedRecordsGetFirstWhereKeyEqualsWithNumericKey() { - // Setup - Opportunity deal = new Opportunity(Name = 'First'); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(new List{ deal }); - related.groupUnderKey('42', deal); - - // Test - SObject result = related.getFirstWhereKeyEquals(42); - - // Verify - Assert.areEqual(deal, result, 'The record grouped under the text key should be returned.'); - } - - @IsTest - static void providedRecordsGroupUnderKeyWithNullKey() { - // Setup - Opportunity orphan = new Opportunity(Name = 'Orphan'); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(new List{ orphan }); - related.groupUnderKey(null, orphan); - - // Test - List result = related.getAllWhereKeyEquals(null); - - // Verify - Assert.areEqual(new List(), result, 'The record should not be grouped.'); - } - - @IsTest - static void providedRecordsGetRecordsWithUngroupedRecord() { - // Setup - Opportunity orphan = new Opportunity(Name = 'Orphan'); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(new List{ orphan }); - related.groupUnderKey(null, orphan); - - // Test - List result = related.getRecords(); - - // Verify - Assert.areEqual(new List{ orphan }, result, 'The ungrouped record should be returned.'); - } - - @IsTest - static void providedRecordsIsEmptyWithUngroupedRecord() { - // Setup - Opportunity orphan = new Opportunity(Name = 'Orphan'); - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(new List{ orphan }); - related.groupUnderKey(null, orphan); - - // Test - Boolean result = related.isEmpty(); - - // Verify - Assert.isFalse(result, 'The records should not be empty.'); - } - - @IsTest - static void providedRecordsGetRecordsWhenBuiltFromNull() { - // Setup - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(null); - - // Test - List result = related.getRecords(); - - // Verify - Assert.areEqual(new List(), result, 'An empty list should be returned.'); - } - - @IsTest - static void getRelated() { - // Setup - TriggerTypes.ProvidedRecords openOpportunities = new TriggerTypes.ProvidedRecords(new List{ new Opportunity(Name = 'Open') }); - TriggerTypes.ProvidedRecords closedOpportunities = new TriggerTypes.ProvidedRecords(new List{ new Opportunity(Name = 'Closed') }); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), null); - record.setRelated(new Map{ 'openOpportunities' => openOpportunities, 'closedOpportunities' => closedOpportunities }); - - // Test - TriggerTypes.RelatedRecords result = record.getRelated('openOpportunities'); - - // Verify - Assert.areEqual(openOpportunities, result, 'The related records should be returned.'); - } - - @IsTest - static void getRelatedWithRelatedLoader() { - // Setup - TestRelatedLoader relatedLoader = new TestRelatedLoader(); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), null); - record.setRelated(new Map{ 'openOpportunities' => new TriggerTypes.ProvidedRecords(new List()) }); - record.setRelatedLoader(relatedLoader); - - // Test - record.getRelated('openOpportunities'); - - // Verify - Assert.areEqual(new List{ 'openOpportunities' }, relatedLoader.loadedProviderNames, 'The related records should be loaded before they are read.'); - } - - @IsTest - static void getRelatedWhenNameIsUnknown() { - // Setup - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), null); - record.setRelated(new Map{ 'openOpportunities' => new TriggerTypes.ProvidedRecords(new List()) }); - Exception expectedException = null; - - // Test - try { - record.getRelated('missing'); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'No related records provider named missing is declared. Return it from the queryRelatedOn method of the handler first.', - expectedException.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void getRelatedWhenNoProvidersAreSet() { - // Setup - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), null); - Exception expectedException = null; - - // Test - try { - record.getRelated('openOpportunities'); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'No related records provider named openOpportunities is declared. Return it from the queryRelatedOn method of the handler first.', - expectedException.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void setRelatedWhenAlreadySet() { - // Setup - TriggerTypes.ProvidedRecords related = new TriggerTypes.ProvidedRecords(new List()); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'New'), null); - record.setRelated(new Map{ 'first' => related }); - record.setRelated(new Map{ 'second' => related }); - Exception expectedException = null; - - // Test - try { - record.getRelated('first'); - } catch (Exception e) { - expectedException = e; - } - - // Verify - Assert.areEqual( - 'No related records provider named first is declared. Return it from the queryRelatedOn method of the handler first.', - expectedException.getMessage(), - 'The exception message should match the expected one.' - ); - } - - @IsTest - static void updateRecordsGetIdsWhenRecordHasNoId() { - // Setup - Id contactId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords( - new List{ - new TriggerTypes.TriggerRecord(new Contact(Id = contactId, LastName = 'Saved'), new Contact(Id = contactId, LastName = 'Old')), - new TriggerTypes.TriggerRecord(new Contact(LastName = 'Unsaved'), null) - } - ); - - // Test - Set ids = records.getIds(); - - // Verify - Assert.areEqual(new Set{ contactId }, ids, 'The record without an Id should be skipped.'); - } - - @IsTest - static void updateRecordsSizeWhenRecordHasNoId() { - // Setup - Id contactId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords( - new List{ - new TriggerTypes.TriggerRecord(new Contact(Id = contactId, LastName = 'Saved'), new Contact(Id = contactId, LastName = 'Old')), - new TriggerTypes.TriggerRecord(new Contact(LastName = 'Unsaved'), null) - } - ); - - // Test - Integer result = records.size(); - - // Verify - Assert.areEqual(2, result, 'The size should include every record.'); - } - - @IsTest - static void updateRecordsGetIdsOf() { - // Setup - Id oldAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Id newAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords( - new List{ - new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', AccountId = newAccountId), new Contact(LastName = 'A', AccountId = oldAccountId)) - } - ); - - // Test - Set ids = records.getIdsOf(Contact.AccountId); - - // Verify - Assert.areEqual(new Set{ newAccountId }, ids, 'The new Ids should be returned.'); - } - - @IsTest - static void updateRecordsGetIdsOfWhenValueIsNull() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords( - new List{ - new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', AccountId = accountId), new Contact(LastName = 'A', AccountId = accountId)), - new TriggerTypes.TriggerRecord(new Contact(LastName = 'B'), new Contact(LastName = 'B')) - } - ); - - // Test - Set ids = records.getIdsOf(Contact.AccountId); - - // Verify - Assert.areEqual(new Set{ accountId }, ids, 'The null value should be skipped.'); - } - - @IsTest - static void updateRecordsGetOldIdsOf() { - // Setup - Id oldAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Id newAccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords( - new List{ - new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', AccountId = newAccountId), new Contact(LastName = 'A', AccountId = oldAccountId)) - } - ); - - // Test - Set oldIds = records.getOldIdsOf(Contact.AccountId); - - // Verify - Assert.areEqual(new Set{ oldAccountId }, oldIds, 'The old Ids should be returned.'); - } - - @IsTest - static void updateRecordsGetOldValuesOf() { - // Setup - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords( - new List{ new TriggerTypes.TriggerRecord(new Contact(LastName = 'Current'), new Contact(LastName = 'Previous')) } - ); - - // Test - Set oldValues = records.getOldValuesOf(Contact.LastName); - - // Verify - Assert.areEqual(new Set{ 'Previous' }, oldValues, 'The old values should be returned.'); - } - - @IsTest - static void deleteRecordsGetIdsOf() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.DeleteRecords records = new TriggerTypes.DeleteTriggerRecords( - new List{ new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'Gone', AccountId = accountId)) } - ); - - // Test - Set ids = records.getIdsOf(Contact.AccountId); - - // Verify - Assert.areEqual(new Set{ accountId }, ids, 'The old Ids should be returned.'); - } - - @IsTest - static void undeleteRecordsGetValuesOfWithNumberField() { - // Setup - TriggerTypes.UndeleteRecords records = new TriggerTypes.UndeleteTriggerRecords( - new List{ new TriggerTypes.TriggerRecord(new Opportunity(Name = 'One', Amount = 100), null) } - ); - - // Test - Set amounts = records.getValuesOf(Opportunity.Amount); - - // Verify - Assert.areEqual(new Set{ '100' }, amounts, 'The value should be returned as text.'); - } - - @IsTest - static void undeleteRecordsGetValuesOfWhenValueIsNull() { - // Setup - TriggerTypes.UndeleteRecords records = new TriggerTypes.UndeleteTriggerRecords( - new List{ - new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', Title = 'Manager'), null), - new TriggerTypes.TriggerRecord(new Contact(LastName = 'B'), null) - } - ); - - // Test - Set titles = records.getValuesOf(Contact.Title); - - // Verify - Assert.areEqual(new Set{ 'Manager' }, titles, 'The null value should be skipped.'); - } - - @IsTest - static void insertRecordsGetIdsOfWithRelationship() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Id ownerId = new TriggerTypes.RandomIdGenerator().get(User.SObjectType); - TriggerTypes.TriggerRecord enriched = new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', AccountId = accountId), null); - enriched.setNewParent('Account', new Account(Id = accountId, OwnerId = ownerId)); - TriggerTypes.InsertRecords records = new TriggerTypes.InsertTriggerRecords(new List{ enriched }); - - // Test - Set ownerIds = records.getIdsOf('Account', Account.OwnerId); - - // Verify - Assert.areEqual(new Set{ ownerId }, ownerIds, 'The parent Ids should be returned.'); - } - - @IsTest - static void updateRecordsGetValuesOfWithDottedRelationship() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Account parentAccount = new Account(Id = accountId, Name = 'Child Corp'); - parentAccount.putSObject('Parent', new Account(Name = 'Holding Corp')); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', AccountId = accountId), new Contact(LastName = 'A', AccountId = accountId)); - record.setNewParent('Account', parentAccount); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords(new List{ record }); - - // Test - Set names = records.getValuesOf('Account.Parent', Account.Name); - - // Verify - Assert.areEqual(new Set{ 'Holding Corp' }, names, 'The grandparent values should be returned.'); - } - - @IsTest - static void updateRecordsGetOldValuesOfWithRelationship() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', AccountId = accountId), new Contact(LastName = 'A', AccountId = accountId)); - record.setNewParent('Account', new Account(Id = accountId, Name = 'Current')); - record.setOldParent('Account', new Account(Id = accountId, Name = 'Previous')); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords(new List{ record }); - - // Test - Set oldNames = records.getOldValuesOf('Account', Account.Name); - - // Verify - Assert.areEqual(new Set{ 'Previous' }, oldNames, 'The old parent values should be returned.'); - } - - @IsTest - static void updateRecordsGetOldIdsOfWithRelationship() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - Id currentOwnerId = new TriggerTypes.RandomIdGenerator().get(User.SObjectType); - Id previousOwnerId = new TriggerTypes.RandomIdGenerator().get(User.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', AccountId = accountId), new Contact(LastName = 'A', AccountId = accountId)); - record.setNewParent('Account', new Account(Id = accountId, OwnerId = currentOwnerId)); - record.setOldParent('Account', new Account(Id = accountId, OwnerId = previousOwnerId)); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords(new List{ record }); - - // Test - Set oldOwnerIds = records.getOldIdsOf('Account', Account.OwnerId); - - // Verify - Assert.areEqual(new Set{ previousOwnerId }, oldOwnerIds, 'The old parent Ids should be returned.'); - } - - @IsTest - static void updateRecordsGetValuesOfWithParentLoader() { - // Setup - TestParentLoader parentLoader = new TestParentLoader(); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'A'), new Contact(LastName = 'A')); - record.setParentLoader(parentLoader); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords(new List{ record }); - - // Test - records.getValuesOf('Account.Parent', Account.Name); - - // Verify - Assert.areEqual(new List{ 'Account' }, parentLoader.loadedRelationshipNames, 'The parent should be loaded before it is read.'); - } - - @IsTest - static void updateRecordsGetOldValuesOfWithParentLoader() { - // Setup - TestParentLoader parentLoader = new TestParentLoader(); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(new Contact(LastName = 'A'), new Contact(LastName = 'A')); - record.setParentLoader(parentLoader); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords(new List{ record }); - - // Test - records.getOldValuesOf('Account', Account.Name); - - // Verify - Assert.areEqual(new List{ 'Account' }, parentLoader.loadedRelationshipNames, 'The parent should be loaded before it is read.'); - } - - @IsTest - static void deleteRecordsGetValuesOfWithRelationship() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.TriggerRecord record = new TriggerTypes.TriggerRecord(null, new Contact(LastName = 'A', AccountId = accountId)); - record.setOldParent('Account', new Account(Id = accountId, Name = 'Previous')); - TriggerTypes.DeleteRecords records = new TriggerTypes.DeleteTriggerRecords(new List{ record }); - - // Test - Set names = records.getValuesOf('Account', Account.Name); - - // Verify - Assert.areEqual(new Set{ 'Previous' }, names, 'The old parent values should be returned.'); - } - - @IsTest - static void insertRecordsGetIdsOfWhenRelationshipIsNotEnriched() { - // Setup - TriggerTypes.InsertRecords records = new TriggerTypes.InsertTriggerRecords( - new List{ - new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', AccountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType)), null) - } - ); - - // Test - Set ids = records.getIdsOf('Account', Account.OwnerId); - - // Verify - Assert.areEqual(new Set(), ids, 'No Ids should be returned.'); - } - - @IsTest - static void insertRecordsGetIdsOfWhenPreviousResultIsCleared() { - // Setup - Id accountId = new TriggerTypes.RandomIdGenerator().get(Account.SObjectType); - TriggerTypes.InsertRecords records = new TriggerTypes.InsertTriggerRecords( - new List{ new TriggerTypes.TriggerRecord(new Contact(LastName = 'A', AccountId = accountId), null) } - ); - records.getIdsOf(Contact.AccountId).clear(); - - // Test - Set ids = records.getIdsOf(Contact.AccountId); - - // Verify - Assert.areEqual(new Set{ accountId }, ids, 'A new set should be returned.'); - } - - @IsTest - static void insertRecordsGetRecords() { - // Setup - TriggerTypes.TriggerRecord firstRecord = new TriggerTypes.TriggerRecord(new Contact(LastName = 'A'), null); - TriggerTypes.TriggerRecord secondRecord = new TriggerTypes.TriggerRecord(new Contact(LastName = 'B'), null); - TriggerTypes.InsertRecords records = new TriggerTypes.InsertTriggerRecords(new List{ firstRecord, secondRecord }); - - // Test - List result = records.getRecords(); - - // Verify - Assert.areEqual(new List{ firstRecord, secondRecord }, result, 'The records should be returned in trigger order.'); - } - - @IsTest - static void updateRecordsGetRecords() { - // Setup - Id firstContactId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - Id secondContactId = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType); - TriggerTypes.TriggerRecord firstRecord = new TriggerTypes.TriggerRecord(new Contact(Id = firstContactId, LastName = 'A'), new Contact(Id = firstContactId, LastName = 'A')); - TriggerTypes.TriggerRecord secondRecord = new TriggerTypes.TriggerRecord( - new Contact(Id = secondContactId, LastName = 'B'), - new Contact(Id = secondContactId, LastName = 'B') - ); - TriggerTypes.UpdateRecords records = new TriggerTypes.UpdateTriggerRecords(new List{ firstRecord, secondRecord }); - - // Test - List result = records.getRecords(); - - // Verify - Assert.areEqual(new List{ firstRecord, secondRecord }, result, 'The records should be returned in trigger order.'); - } - - @IsTest - static void deleteRecordsGetRecords() { - // Setup - TriggerTypes.TriggerRecord firstRecord = new TriggerTypes.TriggerRecord( - null, - new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'A') - ); - TriggerTypes.TriggerRecord secondRecord = new TriggerTypes.TriggerRecord( - null, - new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'B') - ); - TriggerTypes.DeleteRecords records = new TriggerTypes.DeleteTriggerRecords(new List{ firstRecord, secondRecord }); - - // Test - List result = records.getRecords(); - - // Verify - Assert.areEqual(new List{ firstRecord, secondRecord }, result, 'The records should be returned in trigger order.'); - } - - @IsTest - static void undeleteRecordsGetRecords() { - // Setup - TriggerTypes.TriggerRecord firstRecord = new TriggerTypes.TriggerRecord( - new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'A'), - null - ); - TriggerTypes.TriggerRecord secondRecord = new TriggerTypes.TriggerRecord( - new Contact(Id = new TriggerTypes.RandomIdGenerator().get(Contact.SObjectType), LastName = 'B'), - null - ); - TriggerTypes.UndeleteRecords records = new TriggerTypes.UndeleteTriggerRecords(new List{ firstRecord, secondRecord }); - - // Test - List result = records.getRecords(); - - // Verify - Assert.areEqual(new List{ firstRecord, secondRecord }, result, 'The records should be returned in trigger order.'); - } - - @IsTest - static void toTriggerRecords() { - // Setup - List newContacts = new List{ new Contact(LastName = 'First'), new Contact(LastName = 'Second') }; - List oldContacts = new List{ new Contact(LastName = 'First Before'), new Contact(LastName = 'Second Before') }; - - // Test - List records = TriggerTypes.toTriggerRecords(newContacts, oldContacts); - - // Verify - Assert.isTrue(records[1].isChangedFromTo(Contact.LastName, 'Second Before', 'Second'), 'The new and old records should be paired by position.'); - } - - private class TestRelatedLoader implements TriggerTypes.RelatedLoader { - public List loadedProviderNames = new List(); - - public void load(String providerName) { - this.loadedProviderNames.add(providerName); - } - } - - private class TestParentLoader implements TriggerTypes.ParentLoader { - public List loadedRelationshipNames = new List(); - - public void load(String relationshipName) { - this.loadedRelationshipNames.add(relationshipName); - } - } -} diff --git a/package/main/default/classes/TriggerTypesTest.cls-meta.xml b/package/main/default/classes/TriggerTypesTest.cls-meta.xml deleted file mode 100644 index 7d3a8b2..0000000 --- a/package/main/default/classes/TriggerTypesTest.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 67.0 - Active - diff --git a/package/main/default/objects/TriggerHandler__mdt/TriggerHandler__mdt.object-meta.xml b/package/main/default/objects/TriggerHandler__mdt/TriggerHandler__mdt.object-meta.xml deleted file mode 100644 index 3bf0a5f..0000000 --- a/package/main/default/objects/TriggerHandler__mdt/TriggerHandler__mdt.object-meta.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - Trigger Handlers - Public - diff --git a/package/main/default/objects/TriggerHandler__mdt/fields/ApexClassName__c.field-meta.xml b/package/main/default/objects/TriggerHandler__mdt/fields/ApexClassName__c.field-meta.xml deleted file mode 100644 index 79f0720..0000000 --- a/package/main/default/objects/TriggerHandler__mdt/fields/ApexClassName__c.field-meta.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - ApexClassName__c - SubscriberControlled - - 255 - true - Text - false - diff --git a/package/main/default/objects/TriggerHandler__mdt/fields/Bypass__c.field-meta.xml b/package/main/default/objects/TriggerHandler__mdt/fields/Bypass__c.field-meta.xml deleted file mode 100644 index d6c5b7c..0000000 --- a/package/main/default/objects/TriggerHandler__mdt/fields/Bypass__c.field-meta.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Bypass__c - false - SubscriberControlled - - Checkbox - diff --git a/package/main/default/objects/TriggerHandler__mdt/fields/TriggerObject__c.field-meta.xml b/package/main/default/objects/TriggerHandler__mdt/fields/TriggerObject__c.field-meta.xml deleted file mode 100644 index db0110f..0000000 --- a/package/main/default/objects/TriggerHandler__mdt/fields/TriggerObject__c.field-meta.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - TriggerObject__c - SubscriberControlled - - TriggerObject__mdt - Trigger Handlers - TriggerHandlers - true - MetadataRelationship - diff --git a/package/main/default/objects/TriggerObject__mdt/TriggerObject__mdt.object-meta.xml b/package/main/default/objects/TriggerObject__mdt/TriggerObject__mdt.object-meta.xml deleted file mode 100644 index 84c6753..0000000 --- a/package/main/default/objects/TriggerObject__mdt/TriggerObject__mdt.object-meta.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - Trigger Objects - Public - diff --git a/package/main/default/objects/TriggerObject__mdt/fields/Bypass__c.field-meta.xml b/package/main/default/objects/TriggerObject__mdt/fields/Bypass__c.field-meta.xml deleted file mode 100644 index d6c5b7c..0000000 --- a/package/main/default/objects/TriggerObject__mdt/fields/Bypass__c.field-meta.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Bypass__c - false - SubscriberControlled - - Checkbox - diff --git a/package/main/default/objects/TriggerObject__mdt/fields/ObjectAPIName__c.field-meta.xml b/package/main/default/objects/TriggerObject__mdt/fields/ObjectAPIName__c.field-meta.xml deleted file mode 100644 index b615fbd..0000000 --- a/package/main/default/objects/TriggerObject__mdt/fields/ObjectAPIName__c.field-meta.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - ObjectAPIName__c - SubscriberControlled - - 255 - true - Text - false - diff --git a/scripts/build-dependencies.mjs b/scripts/build-dependencies.mjs new file mode 100644 index 0000000..d72b9a2 --- /dev/null +++ b/scripts/build-dependencies.mjs @@ -0,0 +1,76 @@ +// Fills force-app/main/default/dependencies/ with the public SOQL Lib and DML Lib +// classes Trigger Lib needs, at the versions pinned in the package dependencies +// of sfdx-project.json. That way a plain clone of this repo deploys to any org. +// +// Never edit dependencies/ by hand. After bumping a dependency in +// sfdx-project.json, run this and commit the result: +// npm run build:dependencies +// CI rebuilds the folder and fails when the committed copy does not match. + +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { basename, dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = fileURLToPath(new URL('..', import.meta.url)); +const PACKAGE_DIR = 'force-app'; +const TARGET_DIR = join(ROOT, PACKAGE_DIR, 'main', 'default', 'dependencies'); +const OWNER = 'beyond-the-cloud-dev'; + +// Public files of each dependency that Trigger Lib needs, as paths in that repo. +// A new dependency in sfdx-project.json has to be added here too. +const DEPENDENCY_FILES = { + 'SOQL Lib': [ + 'force-app/main/default/classes/standard-soql/SOQL.cls', + 'force-app/main/default/classes/standard-soql/SOQL_Test.cls' + ], + 'DML Lib': [ + 'force-app/main/default/classes/DML.cls', + 'force-app/main/default/classes/DML_Test.cls' + ] +}; + +function packageDependencies() { + const project = JSON.parse( + readFileSync(join(ROOT, 'sfdx-project.json'), 'utf8') + ); + const packageDir = project.packageDirectories.find( + dir => dir.path === PACKAGE_DIR + ); + return (packageDir.dependencies ?? []).map( + ({ package: alias, versionNumber }) => { + const [name, aliasVersion] = alias.split('@'); + const version = (aliasVersion ?? versionNumber ?? '').match( + /^\d+\.\d+\.\d+/ + )?.[0]; + if (!version) { + throw new Error(`Cannot read a version from dependency "${alias}"`); + } + return { name, version, repo: name.toLowerCase().replaceAll(' ', '-') }; + } + ); +} + +async function fetchText(url) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`${response.status} for ${url}`); + } + return response.text(); +} + +rmSync(TARGET_DIR, { recursive: true, force: true }); +for (const { name, version, repo } of packageDependencies()) { + const files = DEPENDENCY_FILES[name]; + if (!files) { + throw new Error( + `No DEPENDENCY_FILES entry for "${name}" in scripts/build-dependencies.mjs` + ); + } + for (const file of files.flatMap(path => [path, `${path}-meta.xml`])) { + const url = `https://raw.githubusercontent.com/${OWNER}/${repo}/refs/tags/v${version}/${file}`; + const target = join(TARGET_DIR, repo, basename(file)); + mkdirSync(dirname(target), { recursive: true }); + writeFileSync(target, await fetchText(url)); + } + console.log(`${name} ${version} from ${OWNER}/${repo}@v${version}`); +} diff --git a/scripts/prepare-package.mjs b/scripts/prepare-package.mjs new file mode 100644 index 0000000..4b75da1 --- /dev/null +++ b/scripts/prepare-package.mjs @@ -0,0 +1,81 @@ +// Turns force-app/ into the package source, in place. Run it only on a throwaway +// checkout (CI), never on your working copy: +// - drops main/default/dependencies/: SOQL Lib and DML Lib come in as package +// dependencies declared in sfdx-project.json +// - every declaration with a `// global` line right above it becomes global +// (@TestVisible on it is dropped, it means nothing on a global member) +// +// The repo keeps the public source, so a plain clone deploys to any org. The +// `// global` markers are the package API: add one when a new type or member has +// to be visible to orgs that install the package. + +import { readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = fileURLToPath(new URL('..', import.meta.url)); +const SOURCE_DIR = join(ROOT, 'force-app'); +const DEPENDENCIES_DIR = join(SOURCE_DIR, 'main', 'default', 'dependencies'); + +const MARKER = /^\s*\/\/ global\s*$/; +const TEST_VISIBLE = /^\s*@TestVisible\s*$/i; +const ANNOTATION = /^\s*@\w+/; +const DECLARATION = /^(\s*)(public|private)\b/; + +function listClasses(dir) { + return readdirSync(dir, { withFileTypes: true }).flatMap(entry => { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + return listClasses(path); + } + return path.endsWith('.cls') ? [path] : []; + }); +} + +function globalize(file) { + const out = []; + let marked = false; + let count = 0; + readFileSync(file, 'utf8') + .split('\n') + .forEach((line, index) => { + if (MARKER.test(line)) { + marked = true; + out.push(line); + return; + } + if (!marked) { + out.push(line); + return; + } + if (TEST_VISIBLE.test(line)) { + return; + } + if (ANNOTATION.test(line)) { + out.push(line); + return; + } + if (!DECLARATION.test(line)) { + throw new Error( + `${relative(ROOT, file)}:${index + 1}: "// global" is not followed by a public or private declaration` + ); + } + out.push(line.replace(DECLARATION, '$1global')); + marked = false; + count++; + }); + if (marked) { + throw new Error( + `${relative(ROOT, file)}: "// global" at the end of the file` + ); + } + writeFileSync(file, out.join('\n')); + return count; +} + +rmSync(DEPENDENCIES_DIR, { recursive: true, force: true }); +const total = listClasses(SOURCE_DIR).reduce( + (sum, file) => sum + globalize(file), + 0 +); +console.log(`${total} declarations made global, dependencies/ removed`); diff --git a/sfdx-project.json b/sfdx-project.json index a4c344f..f50bb92 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -3,7 +3,7 @@ { "versionName": "ver 0.2", "versionNumber": "0.2.0.NEXT", - "path": "package", + "path": "force-app", "default": true, "package": "Trigger Lib", "versionDescription": "", @@ -20,10 +20,6 @@ { "path": "examples", "default": false - }, - { - "path": "force-app", - "default": false } ], "name": "trigger-lib", @@ -37,4 +33,4 @@ "Trigger Lib@0.1.0-1": "04tP6000003jdrVIAQ", "Trigger Lib@0.2.0-1": "04tP6000003jhdNIAQ" } -} \ No newline at end of file +} diff --git a/website/installation.md b/website/installation.md index abcefad..9643873 100644 --- a/website/installation.md +++ b/website/installation.md @@ -4,6 +4,15 @@ description: Install Trigger Lib in a Salesforce org - deploy via button, or cop # Installation +::: info Dependencies +Trigger Lib is built on two other Apex Fluently libs, so they have to be in the org too: + +- [SOQL Lib](https://soql.beyondthecloud.dev) 6.12.0 +- [DML Lib](https://dml.beyondthecloud.dev) 4.0.0 + +Every install option below says how to get them. +::: +