拡張トランザクションセキュリティの Apex テスト
| 使用可能なインターフェース: Salesforce Classic および Lightning Experience |
| 使用可能なエディション: Enterprise Edition、Unlimited Edition、および Developer Edition Salesforce Shield または Salesforce Event Monitoring アドオンサブスクリプションが必要です。 |
開始するために単体テストの例を見てみましょう。次の Apex ポリシーをテストするとします。
1global class LeadExportEventCondition implements TxnSecurity.EventCondition {
2 public boolean evaluate(SObject event) {
3 switch on event{
4 when ApiEvent apiEvent {
5 return evaluate(apiEvent.QueriedEntities, apiEvent.RowsProcessed);
6 }
7 when ReportEvent reportEvent {
8 return evaluate(reportEvent.QueriedEntities, reportEvent.RowsProcessed);
9 }
10 when ListViewEvent listViewEvent {
11 return evaluate(listViewEvent.QueriedEntities, listViewEvent.RowsProcessed);
12 }
13 when null {
14 return false;
15 }
16 when else {
17 return false;
18 }
19 }
20 }
21
22 private boolean evaluate(String queriedEntities, Decimal rowsProcessed){
23 if (queriedEntities.contains('Lead') && rowsProcessed > 2000){
24 return true;
25 }
26 return false;
27 }
28}テストの計画および記述
テストの記述を開始する前に、テスト計画で対象とするプラスとマイナスの使用事例の概要を確認しましょう。
| evaluate メソッドが受信した場合... | かつ... | evaluate メソッドが返す... |
|---|---|---|
| ApiEvent オブジェクト | ApiEvent で Lead がその QueriedEntities 項目にあり、2000 より大きい数値が RowsProcessed 項目にある | true |
| ReportEvent オブジェクト | ReportEvent で Lead がその QueriedEntities 項目にあり、2000 より大きい数値が RowsProcessed 項目にある | true |
| ListViewEvent オブジェクト | ListViewEvent で Lead がその QueriedEntities 項目にあり、2000 より大きい数値が RowsProcessed 項目にある | true |
| 任意のイベントオブジェクト | イベントで Lead がその QueriedEntities 項目になく、2000 より大きい数値が RowsProcessed 項目にある | false |
| 任意のイベントオブジェクト | イベントで Lead がその QueriedEntities 項目にあり、2000 以下の数値が RowsProcessed 項目にある | false |
| 任意のイベントオブジェクト | イベントで Lead がその QueriedEntities 項目になく、2000 以下の数値が RowsProcessed 項目にある | false |
| evaluate メソッドが受信した場合... | かつ... | evaluate メソッドが返す... |
|---|---|---|
| LoginEvent オブジェクト | (条件なし) | false |
| null 値 | (条件なし) | false |
| ApiEvent オブジェクト | QueriedEntities 項目が null である | false |
| ReportEvent オ��ジェクト | RowsProcessed 項目が null である | false |
次に、こうしたすべての使用事例を実装する Apex テストコードを示します。
1/**
2 * Tests for the LeadExportEventCondition class, to make sure that our Transaction Security Apex
3 * logic handles events and event field values as expected.
4 **/
5 @isTest
6 public class LeadExportEventConditionTest {
7
8 /**
9 * ------------ POSITIVE TEST CASES ------------
10 ** /
11
12 /**
13 * Positive test case 1: If an ApiEvent has Lead as a queried entity and more than 2000 rows
14 * processed, then the evaluate method of our policy's Apex should return true.
15 **/
16 static testMethod void testApiEventPositiveTestCase() {
17 // set up our event and its field values
18 ApiEvent testEvent = new ApiEvent();
19 testEvent.QueriedEntities = 'Account, Lead';
20 testEvent.RowsProcessed = 2001;
21
22 // test that the Apex returns true for this event
23 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
24 System.assert(eventCondition.evaluate(testEvent));
25 }
26
27 /**
28 * Positive test case 2: If a ReportEvent has Lead as a queried entity and more than 2000 rows
29 * processed, then the evaluate method of our policy's Apex should return true.
30 **/
31 static testMethod void testReportEventPositiveTestCase() {
32 // set up our event and its field values
33 ReportEvent testEvent = new ReportEvent();
34 testEvent.QueriedEntities = 'Account, Lead';
35 testEvent.RowsProcessed = 2001;
36
37 // test that the Apex returns true for this event
38 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
39 System.assert(eventCondition.evaluate(testEvent));
40 }
41
42 /**
43 * Positive test case 3: If a ListViewEvent has Lead as a queried entity and more than 2000 rows
44 * processed, then the evaluate method of our policy's Apex should return true.
45 **/
46 static testMethod void testListViewEventPositiveTestCase() {
47 // set up our event and its field values
48 ListViewEvent testEvent = new ListViewEvent();
49 testEvent.QueriedEntities = 'Account, Lead';
50 testEvent.RowsProcessed = 2001;
51
52 // test that the Apex returns true for this event
53 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
54 System.assert(eventCondition.evaluate(testEvent));
55 }
56
57 /**
58 * Positive test case 4: If an event does not have Lead as a queried entity and has more
59 * than 2000 rows processed, then the evaluate method of our policy's Apex
60 * should return false.
61 **/
62 static testMethod void testOtherQueriedEntityPositiveTestCase() {
63 // set up our event and its field values
64 ApiEvent testEvent = new ApiEvent();
65 testEvent.QueriedEntities = 'Account';
66 testEvent.RowsProcessed = 2001;
67
68 // test that the Apex returns false for this event
69 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
70 System.assertEquals(false, eventCondition.evaluate(testEvent));
71 }
72
73 /**
74 * Positive test case 5: If an event has Lead as a queried entity and does not have
75 * more than 2000 rows processed, then the evaluate method of our policy's Apex
76 * should return false.
77 **/
78 static testMethod void testFewerRowsProcessedPositiveTestCase() {
79 // set up our event and its field values
80 ReportEvent testEvent = new ReportEvent();
81 testEvent.QueriedEntities = 'Account, Lead';
82 testEvent.RowsProcessed = 2000;
83
84 // test that the Apex returns false for this event
85 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
86 System.assertEquals(false, eventCondition.evaluate(testEvent));
87 }
88
89 /**
90 * Positive test case 6: If an event does not have Lead as a queried entity and does not have
91 * more than 2000 rows processed, then the evaluate method of our policy's Apex
92 * should return false.
93 **/
94 static testMethod void testNoConditionsMetPositiveTestCase() {
95 // set up our event and its field values
96 ListViewEvent testEvent = new ListViewEvent();
97 testEvent.QueriedEntities = 'Account';
98 testEvent.RowsProcessed = 2000;
99
100 // test that the Apex returns false for this event
101 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
102 System.assertEquals(false, eventCondition.evaluate(testEvent));
103 }
104
105 /**
106 * ------------ NEGATIVE TEST CASES ------------
107 **/
108
109 /**
110 * Negative test case 1: If an event is a type other than ApiEvent, ReportEvent, or ListViewEvent,
111 * then the evaluate method of our policy's Apex should return false.
112 **/
113 static testMethod void testOtherEventObject() {
114 LoginEvent loginEvent = new LoginEvent();
115 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
116 System.assertEquals(false, eventCondition.evaluate(loginEvent));
117 }
118
119 /**
120 * Negative test case 2: If an event is null, then the evaluate method of our policy's
121 * Apex should return false.
122 **/
123 static testMethod void testNullEventObject() {
124 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
125 System.assertEquals(false, eventCondition.evaluate(null));
126 }
127
128 /**
129 * Negative test case 3: If an event has a null QueriedEntities value, then the evaluate method
130 * of our policy's Apex should return false.
131 **/
132 static testMethod void testNullQueriedEntities() {
133 ApiEvent testEvent = new ApiEvent();
134 testEvent.QueriedEntities = null;
135 testEvent.RowsProcessed = 2001;
136
137 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
138 System.assertEquals(false, eventCondition.evaluate(testEvent));
139 }
140
141 /**
142 * Negative test case 4: If an event has a null RowsProcessed value, then the evaluate method
143 * of our policy's Apex should return false.
144 **/
145 static testMethod void testNullRowsProcessed() {
146 ReportEvent testEvent = new ReportEvent();
147 testEvent.QueriedEntities = 'Account, Lead';
148 testEvent.RowsProcessed = null;
149
150 LeadExportEventCondition eventCondition = new LeadExportEventCondition();
151 System.assertEquals(false, eventCondition.evaluate(testEvent));
152 }
153 }テスト実行後のポリシーコードの調整
テストを実行し、testNullQueriedEntities テストケースが失敗してエラー System.NullPointerException: Attempt to de-reference a null object が起こったとします。幸い、テスト��予期しない値または null 値をチェックしないトランザクションセキュリティポリシーの領域が明らかになりました。ポリシーは重要な組織の操作中に実行されるため、重要な機能がブロックされないようにエラーがある場合はポリシーが適切に失敗することを確認します。
こうした null 値を適切に処理する Apex クラスの evaluate メソッドを更新する方法を次に示します。
1private boolean evaluate(String queriedEntities, Decimal rowsProcessed) {
2 boolean containsLead = queriedEntities != null ? queriedEntities.contains('Lead')
3 if (containsLead && rowsProcessed > 2000){
4 return true;
5 }
6 return false;
7}queriedEntities 変数で .contains 操作を実行する前に値が null かどうかを最初にチェックするように、コードを変更しました。この変更により、コードで null オブジェクトが参照解決されなくなります。
- 値または状況を無視して、ポリシーがトリガしないように false を返す。
- true を返して操作をフェイルクローズする。
高度な例
ログインしようとしているユーザのプロファイルを取得する SOQL クエリを使���するより複雑な Apex ポリシーを次に示します。
1global class ProfileIdentityEventCondition implements TxnSecurity.EventCondition {
2
3 // For these powerful profiles, let's prompt users to complete 2FA
4 private Set<String> PROFILES_TO_MONITOR = new Set<String> {
5 'System Administrator',
6 'Custom Admin Profile'
7 };
8
9 public boolean evaluate(SObject event) {
10 LoginEvent loginEvent = (LoginEvent) event;
11 String userId = loginEvent.UserId;
12
13 // get the Profile name from the current users profileId
14 Profile profile = [SELECT Name FROM Profile WHERE Id IN
15 (SELECT profileId FROM User WHERE Id = :userId)];
16
17 // check if the name of the Profile is one of the ones we want to monitor
18 if (PROFILES_TO_MONITOR.contains(profile.Name)) {
19 return true;
20 }
21
22 return false;
23 }
24 }テスト計画は次のようになります。
-
ポジティブテストケース
- ログインしようとしているユーザのプロファイルを監視したい場合は、evaluate メソッドが true を返す。
- ログインしようとしているユーザのプロファイルを監視したくない場合は、evaluate メソッドが false を返す。
-
ネガティブテストケース
- 例外を発生させるプロファイルオブジェクトのクエリを行う場合は、evaluate メソッドが false を返す。
- null を返すプロファイルオブジェクトのクエリを行う場合は、evaluate メソッドが false を返す。
すべての Salesforce ユーザに必ずプロファイルが割り当てられるため、そのネガティブテストを作成する必要はありません。2 つのネガティブテストケースに実際のテストを作成することもできません。これについては、ポリシー自体を更新することで Salesforce が行います。ただし、計画で使用事例を明示歴にリストし、さまざまな状況に対応できるようにします。
ポジティブテストケースは、SQQL クエリの結果のみに依存します。これらのクエリが正しく実行されるようにするために、テストデータも作成します。テストコードを見てみましょう。
1/**
2 * Tests for the ProfileIdentityEventCondition class, to make sure that our
3 * Transaction Security Apex logic handles events and event field values as expected.
4 **/
5 @isTest
6 public class ProfileIdentityEventConditionTest {
7
8 /**
9 * ------------ POSITIVE TEST CASES ------------
10 ** /
11
12 /**
13 * Positive test case 1: Evaluate will return true when user has the "System
14 * Administrator" profile.
15 **/
16 static testMethod void testUserWithSysAdminProfile() {
17 // insert a User for our test which has the System Admin profile
18 Profile profile = [SELECT Id FROM Profile WHERE Name='System Administrator'];
19 assertOnProfile(profile.id, true);
20 }
21
22 /**
23 * Positive test case 2: Evaluate will return true when the user has the "Custom
24 * Admin Profile"
25 **/
26 static testMethod void testUserWithCustomProfile() {
27 // insert a User for our test which has the System Admin profile
28 Profile profile = [SELECT Id FROM Profile WHERE Name='Custom Admin Profile'];
29 assertOnProfile(profile.id, true);
30 }
31
32 /**
33 * Positive test case 3: Evalueate will return false when user doesn't have
34 * a profile we're interested in. In this case we'll be using a profile called
35 * 'Standard User'.
36 **/
37 static testMethod void testUserWithSomeProfile() {
38 // insert a User for our test which has the System Admin profile
39 Profile profile = [SELECT Id FROM Profile WHERE Name='Standard User'];
40 assertOnProfile(profile.id, false);
41 }
42
43 /**
44 * Helper to assert on different profiles.
45 **/
46 static void assertOnProfile(String profileId, boolean expected){
47 User user = createUserWithProfile(profileId);
48 insert user;
49
50 // set up our event and its field values
51 LoginEvent testEvent = new LoginEvent();
52 testEvent.UserId = user.Id;
53
54 // test that the Apex returns true for this event
55 ProfileIdentityEventCondition eventCondition = new ProfileIdentityEventCondition();
56 System.assertEquals(expected, eventCondition.evaluate(testEvent));
57 }
58
59 /**
60 * Helper to create a user with the given profileId.
61 **/
62 static User createUserWithProfile(String profileId){
63 // Usernames have to be unique.
64 String username = 'ProfileIdentityEventCondition@Test.com';
65
66 User user = new User(Alias = 'standt', Email='standarduser@testorg.com',
67 EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
68 LocaleSidKey='en_US', ProfileId = profileId,
69 TimeZoneSidKey='America/Los_Angeles', UserName=username);
70 return user;
71 }
72 }プロファイルオブジェクトのクエリを行うときに例外または null の結果をチェックするようにトランザクションセキュリティポリシーコードを更新して、2 つのネガティブテストケースを処理しましょう。
1global class ProfileIdentityEventCondition implements TxnSecurity.EventCondition {
2
3 // For these powerful profiles, let's prompt users to complete 2FA
4 private Set<String> PROFILES_TO_MONITOR = new Set<String> {
5 'System Administrator',
6 'Custom Admin Profile'
7 };
8
9 public boolean evaluate(SObject event) {
10 try{
11 LoginEvent loginEvent = (LoginEvent) event;
12 String userId = loginEvent.UserId;
13
14 // get the Profile name from the current users profileId
15 Profile profile = [SELECT Name FROM Profile WHERE Id IN
16 (SELECT profileId FROM User WHERE Id = :userId)];
17
18 if (profile == null){
19 return false;
20 }
21
22 // check if the name of the Profile is one of the ones we want to monitor
23 if (PROFILES_TO_MONITOR.contains(profile.Name)) {
24 return true;
25 }
26 return false;
27 } catch(Exception ex){
28 System.debug('Exception: ' + ex);
29 return false;
30 }
31 }
32 }