Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion force-app/main/default/classes/AfterDeleteTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class
Expand Down
80 changes: 78 additions & 2 deletions force-app/main/default/classes/AfterInsertTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class
Expand Down Expand Up @@ -344,6 +344,27 @@ private class AfterInsertTest {
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<TriggerObject__mdt>());
SOQL.mock('ApexTypeImplementor').thenReturn(new List<ApexTypeImplementor>());

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
Expand All @@ -369,7 +390,7 @@ private class AfterInsertTest {

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(Contact.SObjectType).thenReturn(new Contact(Id = doe.Id, Account = acme));
SOQL.mock('SavedTriggerRecords').thenReturn(new Contact(Id = doe.Id, Account = acme));

TriggerOrchestrator.mock().afterInsertFor(AccountNameWriter.class).with(doe);

Expand Down Expand Up @@ -562,6 +583,20 @@ private class AfterInsertTest {
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
Expand All @@ -579,6 +614,25 @@ private class AfterInsertTest {
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<TriggerObject__mdt>());
SOQL.mock('ApexTypeImplementor').thenReturn(new List<ApexTypeImplementor>());
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
Expand All @@ -603,6 +657,18 @@ private class AfterInsertTest {
}
}

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;
Expand Down Expand Up @@ -677,6 +743,16 @@ private class AfterInsertTest {
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');
Expand Down
4 changes: 2 additions & 2 deletions force-app/main/default/classes/AfterUndeleteTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class
Expand Down Expand Up @@ -369,7 +369,7 @@ private class AfterUndeleteTest {

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(Contact.SObjectType).thenReturn(new Contact(Id = doe.Id, Account = acme));
SOQL.mock('SavedTriggerRecords').thenReturn(new Contact(Id = doe.Id, Account = acme));

TriggerOrchestrator.mock().afterUndeleteFor(AccountNameWriter.class).with(doe);

Expand Down
6 changes: 3 additions & 3 deletions force-app/main/default/classes/AfterUpdateTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class
Expand Down Expand Up @@ -592,7 +592,7 @@ private class AfterUpdateTest {

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(Contact.SObjectType).thenReturn(new Contact(Id = doe.Id, Account = acme));
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'));

Expand Down Expand Up @@ -634,7 +634,7 @@ private class AfterUpdateTest {

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(Contact.SObjectType).thenReturn(new Contact(Id = doe.Id, Account = acme));
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));

Expand Down
2 changes: 1 addition & 1 deletion force-app/main/default/classes/BeforeDeleteTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class
Expand Down
2 changes: 1 addition & 1 deletion force-app/main/default/classes/BeforeInsertTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class
Expand Down
2 changes: 1 addition & 1 deletion force-app/main/default/classes/BeforeUpdateTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class
Expand Down
18 changes: 15 additions & 3 deletions force-app/main/default/classes/TriggerOrchestrator.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE class
Expand Down Expand Up @@ -508,7 +508,7 @@ public with sharing class TriggerOrchestrator {
protected void handleException(Exception e) {
this.logError(e);

if (e instanceof TriggerTypes.TriggerLibException || !this.handlerHooks.continuesOnError()) {
if (e instanceof TriggerTypes.TriggerLibException || e instanceof EventBus.RetryableException || !this.handlerHooks.continuesOnError()) {
throw e;
}
}
Expand Down Expand Up @@ -617,7 +617,13 @@ public with sharing class TriggerOrchestrator {
fieldsToQuery.addAll(this.getFieldPathsForLookup(lookupField));
}

List<SObject> savedRecords = SOQL.of(this.context.sObjectType).with(fieldsToQuery).byIds(this.context.newRecords).systemMode().withoutSharing().toList();
List<SObject> savedRecords = SOQL.of(this.context.sObjectType)
.with(fieldsToQuery)
.byIds(this.context.newRecords)
.systemMode()
.withoutSharing()
.mockId('SavedTriggerRecords')
.toList();

for (SObjectField lookupField : this.lookupsForNewParents) {
Map<Id, SObject> cachedParentById = this.cachedParentsByLookup.get(lookupField);
Expand Down Expand Up @@ -958,6 +964,8 @@ public with sharing class TriggerOrchestrator {
SOQL.mock(parentType).thenReturn(this.typed(this.parentsByType.get(parentType)));
}

SOQL.mock('SavedTriggerRecords').thenReturn(new List<SObject>());

TriggerOrchestrator orchestrator = new TriggerOrchestrator(this);

orchestrator.context.triggerOperation = this.triggerOperation;
Expand All @@ -976,6 +984,10 @@ public with sharing class TriggerOrchestrator {
}

protected List<SObject> assignIds(List<SObject> 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()));
}
Expand Down
2 changes: 1 addition & 1 deletion force-app/main/default/classes/TriggerOrchestratorTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE test class
Expand Down
43 changes: 42 additions & 1 deletion force-app/main/default/classes/TriggerTypes.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* 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)
*
* v.0.1.0
* v0.2.0
*
* PMD False Positives:
* - ExcessivePublicCount: It is a library class and exposes all necessary methods to evaluate a trigger record
Expand Down Expand Up @@ -30,6 +30,9 @@
Boolean equals(SObjectField field, Object value);
Boolean doesNotEqual(SObjectField field, Object value);

Boolean isIn(SObjectField field, Iterable<Object> values);
Boolean isNotIn(SObjectField field, Iterable<Object> values);

Boolean contains(SObjectField field, String value);
Boolean doesNotContain(SObjectField field, String value);
Boolean startsWith(SObjectField field, String value);
Expand Down Expand Up @@ -93,6 +96,9 @@
Boolean equals(SObjectField field, Object value);
Boolean doesNotEqual(SObjectField field, Object value);

Boolean isIn(SObjectField field, Iterable<Object> values);
Boolean isNotIn(SObjectField field, Iterable<Object> values);

Boolean contains(SObjectField field, String value);
Boolean doesNotContain(SObjectField field, String value);
Boolean startsWith(SObjectField field, String value);
Expand Down Expand Up @@ -157,6 +163,9 @@
Boolean equals(SObjectField field, Object value);
Boolean doesNotEqual(SObjectField field, Object value);

Boolean isIn(SObjectField field, Iterable<Object> values);
Boolean isNotIn(SObjectField field, Iterable<Object> values);

Boolean contains(SObjectField field, String value);
Boolean doesNotContain(SObjectField field, String value);
Boolean startsWith(SObjectField field, String value);
Expand Down Expand Up @@ -186,6 +195,7 @@
Boolean areAllChanged(Iterable<SObjectField> 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);

Expand Down Expand Up @@ -240,6 +250,9 @@
Boolean equals(SObjectField field, Object value);
Boolean doesNotEqual(SObjectField field, Object value);

Boolean isIn(SObjectField field, Iterable<Object> values);
Boolean isNotIn(SObjectField field, Iterable<Object> values);

Boolean contains(SObjectField field, String value);
Boolean doesNotContain(SObjectField field, String value);
Boolean startsWith(SObjectField field, String value);
Expand Down Expand Up @@ -269,6 +282,7 @@
Boolean areAllChanged(Iterable<SObjectField> 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);

Expand Down Expand Up @@ -324,6 +338,9 @@
Boolean equals(SObjectField field, Object value);
Boolean doesNotEqual(SObjectField field, Object value);

Boolean isIn(SObjectField field, Iterable<Object> values);
Boolean isNotIn(SObjectField field, Iterable<Object> values);

Boolean contains(SObjectField field, String value);
Boolean doesNotContain(SObjectField field, String value);
Boolean startsWith(SObjectField field, String value);
Expand Down Expand Up @@ -385,6 +402,9 @@
Boolean equals(SObjectField field, Object value);
Boolean doesNotEqual(SObjectField field, Object value);

Boolean isIn(SObjectField field, Iterable<Object> values);
Boolean isNotIn(SObjectField field, Iterable<Object> values);

Boolean contains(SObjectField field, String value);
Boolean doesNotContain(SObjectField field, String value);
Boolean startsWith(SObjectField field, String value);
Expand Down Expand Up @@ -449,6 +469,9 @@
Boolean equals(SObjectField field, Object value);
Boolean doesNotEqual(SObjectField field, Object value);

Boolean isIn(SObjectField field, Iterable<Object> values);
Boolean isNotIn(SObjectField field, Iterable<Object> values);

Boolean contains(SObjectField field, String value);
Boolean doesNotContain(SObjectField field, String value);
Boolean startsWith(SObjectField field, String value);
Expand Down Expand Up @@ -565,7 +588,7 @@

public interface UnitOfWork {
UnitOfWork toInsert(SObject record);
UnitOfWork toInsert(DML.Record record);

Check failure on line 591 in force-app/main/default/classes/TriggerTypes.cls

View workflow job for this annotation

GitHub Actions / salesforce-ci / deploy-and-test

ApexClass TriggerTypes

global methods do not support parameter type of btcdev.DML.Record (4 occurrences)

UnitOfWork toUpdate(SObject record);
UnitOfWork toUpdate(DML.Record record);
Expand Down Expand Up @@ -674,6 +697,20 @@
return !this.equals(field, value);
}

public Boolean isIn(SObjectField field, Iterable<Object> values) {
for (Object value : values) {
if (this.equals(field, value)) {
return true;
}
}

return false;
}

public Boolean isNotIn(SObjectField field, Iterable<Object> values) {
return !this.isIn(field, values);
}

public Boolean contains(SObjectField field, String value) {
return this.isNotNull(field) && this.getFieldValue(field).toString().contains(value);
}
Expand Down Expand Up @@ -774,6 +811,10 @@
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;
}
Expand Down
Loading
Loading