scc-universal 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +44 -0
- package/.cursor/agents/deep-researcher.md +142 -0
- package/.cursor/agents/doc-updater.md +219 -0
- package/.cursor/agents/eval-runner.md +335 -0
- package/.cursor/agents/learning-engine.md +210 -0
- package/.cursor/agents/loop-operator.md +245 -0
- package/.cursor/agents/refactor-cleaner.md +119 -0
- package/.cursor/agents/sf-admin-agent.md +127 -0
- package/.cursor/agents/sf-agentforce-agent.md +126 -0
- package/.cursor/agents/sf-apex-agent.md +117 -0
- package/.cursor/agents/sf-architect.md +426 -0
- package/.cursor/agents/sf-aura-reviewer.md +369 -0
- package/.cursor/agents/sf-bugfix-agent.md +101 -0
- package/.cursor/agents/sf-flow-agent.md +155 -0
- package/.cursor/agents/sf-integration-agent.md +141 -0
- package/.cursor/agents/sf-lwc-agent.md +123 -0
- package/.cursor/agents/sf-review-agent.md +357 -0
- package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
- package/.cursor/hooks/adapter.js +81 -0
- package/.cursor/hooks/after-file-edit.js +26 -0
- package/.cursor/hooks/after-mcp-execution.js +12 -0
- package/.cursor/hooks/after-shell-execution.js +30 -0
- package/.cursor/hooks/after-tab-file-edit.js +12 -0
- package/.cursor/hooks/before-mcp-execution.js +11 -0
- package/.cursor/hooks/before-read-file.js +13 -0
- package/.cursor/hooks/before-shell-execution.js +29 -0
- package/.cursor/hooks/before-submit-prompt.js +23 -0
- package/.cursor/hooks/pre-compact.js +7 -0
- package/.cursor/hooks/session-end.js +10 -0
- package/.cursor/hooks/session-start.js +10 -0
- package/.cursor/hooks/stop.js +18 -0
- package/.cursor/hooks/subagent-start.js +10 -0
- package/.cursor/hooks/subagent-stop.js +10 -0
- package/.cursor/hooks.json +107 -0
- package/.cursor/skills/aside/SKILL.md +115 -0
- package/.cursor/skills/checkpoint/SKILL.md +50 -0
- package/.cursor/skills/configure-scc/SKILL.md +160 -0
- package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
- package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
- package/.cursor/skills/model-route/SKILL.md +81 -0
- package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
- package/.cursor/skills/refactor-clean/SKILL.md +133 -0
- package/.cursor/skills/resume-session/SKILL.md +111 -0
- package/.cursor/skills/save-session/SKILL.md +183 -0
- package/.cursor/skills/search-first/SKILL.md +140 -0
- package/.cursor/skills/security-scan/SKILL.md +142 -0
- package/.cursor/skills/sessions/SKILL.md +124 -0
- package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
- package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
- package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
- package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
- package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
- package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
- package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
- package/.cursor/skills/sf-api-design/SKILL.md +237 -0
- package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
- package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
- package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
- package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
- package/.cursor/skills/sf-debugging/SKILL.md +362 -0
- package/.cursor/skills/sf-deployment/SKILL.md +291 -0
- package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
- package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
- package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
- package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
- package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
- package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
- package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
- package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
- package/.cursor/skills/sf-help/SKILL.md +156 -0
- package/.cursor/skills/sf-integration/SKILL.md +479 -0
- package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
- package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
- package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
- package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
- package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
- package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
- package/.cursor/skills/sf-security/SKILL.md +330 -0
- package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
- package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
- package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
- package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
- package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
- package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
- package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
- package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
- package/.cursor/skills/strategic-compact/SKILL.md +205 -0
- package/.cursor/skills/update-docs/SKILL.md +162 -0
- package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
- package/.cursor-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/agents/deep-researcher.md +145 -0
- package/agents/doc-updater.md +222 -0
- package/agents/eval-runner.md +340 -0
- package/agents/learning-engine.md +211 -0
- package/agents/loop-operator.md +247 -0
- package/agents/refactor-cleaner.md +122 -0
- package/agents/sf-admin-agent.md +131 -0
- package/agents/sf-agentforce-agent.md +132 -0
- package/agents/sf-apex-agent.md +124 -0
- package/agents/sf-architect.md +435 -0
- package/agents/sf-aura-reviewer.md +372 -0
- package/agents/sf-bugfix-agent.md +105 -0
- package/agents/sf-flow-agent.md +159 -0
- package/agents/sf-integration-agent.md +146 -0
- package/agents/sf-lwc-agent.md +127 -0
- package/agents/sf-review-agent.md +366 -0
- package/agents/sf-visualforce-reviewer.md +468 -0
- package/assets/logo.svg +18 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/authoring-guide.md +373 -0
- package/docs/hook-development.md +578 -0
- package/docs/token-optimization.md +139 -0
- package/docs/workflow-examples.md +645 -0
- package/examples/agentforce-action/README.md +227 -0
- package/examples/apex-trigger-handler/README.md +114 -0
- package/examples/devops-pipeline/README.md +325 -0
- package/examples/flow-automation/README.md +188 -0
- package/examples/integration-pattern/README.md +416 -0
- package/examples/lwc-component/README.md +180 -0
- package/examples/platform-events/README.md +492 -0
- package/examples/scratch-org-setup/README.md +138 -0
- package/examples/security-audit/README.md +244 -0
- package/examples/visualforce-migration/README.md +314 -0
- package/hooks/hooks.json +338 -0
- package/hooks/memory-persistence/README.md +73 -0
- package/manifests/install-modules.json +217 -0
- package/manifests/install-profiles.json +17 -0
- package/mcp-configs/mcp-servers.json +19 -0
- package/package.json +89 -0
- package/schemas/hooks.schema.json +123 -0
- package/schemas/install-modules.schema.json +76 -0
- package/schemas/install-profiles.schema.json +28 -0
- package/schemas/install-state.schema.json +73 -0
- package/schemas/package-manager.schema.json +18 -0
- package/schemas/plugin.schema.json +112 -0
- package/schemas/scc-install-config.schema.json +29 -0
- package/schemas/state-store.schema.json +111 -0
- package/scripts/cli/install-apply.js +170 -0
- package/scripts/cli/uninstall.js +193 -0
- package/scripts/hooks/check-console-log.js +101 -0
- package/scripts/hooks/check-hook-enabled.js +17 -0
- package/scripts/hooks/check-platform-docs-age.js +48 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +98 -0
- package/scripts/hooks/governor-check.js +220 -0
- package/scripts/hooks/learning-observe.sh +206 -0
- package/scripts/hooks/mcp-health-check.js +588 -0
- package/scripts/hooks/post-bash-build-complete.js +34 -0
- package/scripts/hooks/post-bash-pr-created.js +43 -0
- package/scripts/hooks/post-edit-console-warn.js +61 -0
- package/scripts/hooks/post-edit-format.js +79 -0
- package/scripts/hooks/post-edit-typecheck.js +98 -0
- package/scripts/hooks/post-write.js +168 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
- package/scripts/hooks/pre-compact.js +51 -0
- package/scripts/hooks/pre-tool-use.js +163 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +251 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +135 -0
- package/scripts/hooks/session-end-marker.js +29 -0
- package/scripts/hooks/session-end.js +311 -0
- package/scripts/hooks/session-start.js +202 -0
- package/scripts/hooks/sfdx-scanner-check.js +142 -0
- package/scripts/hooks/sfdx-validate.js +119 -0
- package/scripts/hooks/stop-hook.js +170 -0
- package/scripts/hooks/suggest-compact.js +67 -0
- package/scripts/lib/agent-adapter.js +82 -0
- package/scripts/lib/apex-analysis.js +194 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install-config.js +73 -0
- package/scripts/lib/install-executor.js +363 -0
- package/scripts/lib/install-state.js +121 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.js +124 -0
- package/scripts/lib/project-detect.js +228 -0
- package/scripts/lib/schema-validator.js +190 -0
- package/scripts/lib/skill-adapter.js +100 -0
- package/scripts/lib/state-store.js +376 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
- package/scripts/lib/utils.js +313 -0
- package/scripts/scc.js +164 -0
- package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
- package/skills/_reference/APEX_CURSOR.md +159 -0
- package/skills/_reference/API_VERSIONS.md +78 -0
- package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
- package/skills/_reference/ASYNC_PATTERNS.md +163 -0
- package/skills/_reference/AURA_COMPONENTS.md +146 -0
- package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
- package/skills/_reference/DATA_MODELING.md +124 -0
- package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
- package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
- package/skills/_reference/DEPRECATIONS.md +79 -0
- package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
- package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
- package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
- package/skills/_reference/FLOW_PATTERNS.md +113 -0
- package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
- package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
- package/skills/_reference/LWC_PATTERNS.md +79 -0
- package/skills/_reference/METADATA_TYPES.md +115 -0
- package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
- package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
- package/skills/_reference/PLATFORM_EVENTS.md +121 -0
- package/skills/_reference/REPORTING_API.md +143 -0
- package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
- package/skills/_reference/SECURITY_PATTERNS.md +127 -0
- package/skills/_reference/SHARING_MODEL.md +120 -0
- package/skills/_reference/SOQL_PATTERNS.md +119 -0
- package/skills/_reference/TESTING_STANDARDS.md +96 -0
- package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
- package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
- package/skills/aside/SKILL.md +118 -0
- package/skills/checkpoint/SKILL.md +53 -0
- package/skills/configure-scc/SKILL.md +163 -0
- package/skills/continuous-agent-loop/SKILL.md +264 -0
- package/skills/mcp-server-patterns/SKILL.md +146 -0
- package/skills/model-route/SKILL.md +84 -0
- package/skills/prompt-optimizer/SKILL.md +369 -0
- package/skills/refactor-clean/SKILL.md +136 -0
- package/skills/resume-session/SKILL.md +114 -0
- package/skills/save-session/SKILL.md +186 -0
- package/skills/search-first/SKILL.md +144 -0
- package/skills/security-scan/SKILL.md +146 -0
- package/skills/sessions/SKILL.md +127 -0
- package/skills/sf-agentforce-development/SKILL.md +450 -0
- package/skills/sf-apex-async-patterns/SKILL.md +326 -0
- package/skills/sf-apex-best-practices/SKILL.md +425 -0
- package/skills/sf-apex-constraints/SKILL.md +81 -0
- package/skills/sf-apex-cursor/SKILL.md +338 -0
- package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
- package/skills/sf-apex-testing/SKILL.md +409 -0
- package/skills/sf-api-design/SKILL.md +238 -0
- package/skills/sf-approval-processes/SKILL.md +315 -0
- package/skills/sf-aura-development/SKILL.md +263 -0
- package/skills/sf-build-fix/SKILL.md +121 -0
- package/skills/sf-data-modeling/SKILL.md +278 -0
- package/skills/sf-debugging/SKILL.md +363 -0
- package/skills/sf-deployment/SKILL.md +295 -0
- package/skills/sf-deployment-constraints/SKILL.md +155 -0
- package/skills/sf-devops-ci-cd/SKILL.md +325 -0
- package/skills/sf-docs-lookup/SKILL.md +103 -0
- package/skills/sf-e2e-testing/SKILL.md +324 -0
- package/skills/sf-experience-cloud/SKILL.md +249 -0
- package/skills/sf-flow-development/SKILL.md +377 -0
- package/skills/sf-governor-limits/SKILL.md +323 -0
- package/skills/sf-harness-audit/SKILL.md +142 -0
- package/skills/sf-help/SKILL.md +159 -0
- package/skills/sf-integration/SKILL.md +483 -0
- package/skills/sf-lwc-constraints/SKILL.md +130 -0
- package/skills/sf-lwc-development/SKILL.md +303 -0
- package/skills/sf-lwc-testing/SKILL.md +388 -0
- package/skills/sf-metadata-management/SKILL.md +288 -0
- package/skills/sf-platform-events-cdc/SKILL.md +375 -0
- package/skills/sf-quickstart/SKILL.md +173 -0
- package/skills/sf-security/SKILL.md +334 -0
- package/skills/sf-security-constraints/SKILL.md +127 -0
- package/skills/sf-soql-constraints/SKILL.md +131 -0
- package/skills/sf-soql-optimization/SKILL.md +354 -0
- package/skills/sf-tdd-workflow/SKILL.md +336 -0
- package/skills/sf-testing-constraints/SKILL.md +200 -0
- package/skills/sf-trigger-constraints/SKILL.md +90 -0
- package/skills/sf-trigger-frameworks/SKILL.md +347 -0
- package/skills/sf-visualforce-development/SKILL.md +260 -0
- package/skills/strategic-compact/SKILL.md +208 -0
- package/skills/update-docs/SKILL.md +165 -0
- package/skills/update-platform-docs/SKILL.md +90 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-apex-testing
|
|
3
|
+
description: >-
|
|
4
|
+
Apex unit testing — test structure, TestDataFactory, governor limit testing, async testing, mocks, coverage. Use when writing tests or improving coverage. Do NOT use for TDD workflow or LWC Jest tests.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Apex Testing
|
|
8
|
+
|
|
9
|
+
Procedures and patterns for writing effective Apex tests. Constraint rules (never/always lists for test isolation, assertions, SeeAllData) live in `sf-testing-constraints`. This skill covers the _how_ — test structure, factories, mocks, async testing, and coverage strategies.
|
|
10
|
+
|
|
11
|
+
Reference: @../_reference/TESTING_STANDARDS.md
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## When to Use
|
|
16
|
+
|
|
17
|
+
- When writing test classes for new Apex code before deploying to production
|
|
18
|
+
- When existing tests pass but only cover happy paths
|
|
19
|
+
- When a deployment fails with "insufficient code coverage" errors
|
|
20
|
+
- When building mock patterns for callouts or dependency injection
|
|
21
|
+
|
|
22
|
+
> **Related:** For the TDD workflow (red-green-refactor process), see `sf-tdd-workflow`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## @isTest Annotation
|
|
27
|
+
|
|
28
|
+
```apex
|
|
29
|
+
@isTest
|
|
30
|
+
private class AccountServiceTest {
|
|
31
|
+
|
|
32
|
+
@TestSetup
|
|
33
|
+
static void makeData() {
|
|
34
|
+
// Runs once before any test method in this class
|
|
35
|
+
// Each test method gets a fresh transaction with this data
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@isTest
|
|
39
|
+
static void testCreateAccount_validData_createsSuccessfully() {
|
|
40
|
+
// Arrange / Act / Assert
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Test classes do not count toward coverage calculations but DO count toward the 6 MB Apex code character limit. Use `@TestVisible` to make private members accessible in tests without changing access modifiers.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## @TestSetup
|
|
50
|
+
|
|
51
|
+
Runs once per test class. Each test method gets its own database rollback, so modifications in one test do not bleed into another.
|
|
52
|
+
|
|
53
|
+
```apex
|
|
54
|
+
@TestSetup
|
|
55
|
+
static void makeData() {
|
|
56
|
+
Account acc = new Account(
|
|
57
|
+
Name = 'Test Corp',
|
|
58
|
+
Type = 'Customer',
|
|
59
|
+
AnnualRevenue = 1000000,
|
|
60
|
+
Customer_Tier__c = 'Standard'
|
|
61
|
+
);
|
|
62
|
+
insert acc;
|
|
63
|
+
|
|
64
|
+
List<Opportunity> opps = new List<Opportunity>();
|
|
65
|
+
for (Integer i = 0; i < 10; i++) {
|
|
66
|
+
opps.add(new Opportunity(
|
|
67
|
+
Name = 'Test Opp ' + i,
|
|
68
|
+
AccountId = acc.Id,
|
|
69
|
+
StageName = i < 5 ? 'Prospecting' : 'Qualification',
|
|
70
|
+
CloseDate = Date.today().addDays(30 + i),
|
|
71
|
+
Amount = 5000 * (i + 1)
|
|
72
|
+
));
|
|
73
|
+
}
|
|
74
|
+
insert opps;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## TestDataFactory Pattern
|
|
81
|
+
|
|
82
|
+
A central factory class creates test records consistently across the test suite, preventing duplicated record-creation logic.
|
|
83
|
+
|
|
84
|
+
```apex
|
|
85
|
+
@isTest
|
|
86
|
+
public class TestDataFactory {
|
|
87
|
+
|
|
88
|
+
public static Account createAccount() {
|
|
89
|
+
return createAccount(new Map<String, Object>());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public static Account createAccount(Map<String, Object> overrides) {
|
|
93
|
+
Account acc = new Account(
|
|
94
|
+
Name = 'Test Account ' + generateUniqueString(),
|
|
95
|
+
Type = 'Customer',
|
|
96
|
+
Industry = 'Technology',
|
|
97
|
+
AnnualRevenue = 500000,
|
|
98
|
+
Customer_Tier__c = 'Standard'
|
|
99
|
+
);
|
|
100
|
+
applyOverrides(acc, overrides);
|
|
101
|
+
insert acc;
|
|
102
|
+
return acc;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public static List<Account> createAccounts(Integer count) {
|
|
106
|
+
List<Account> accounts = new List<Account>();
|
|
107
|
+
for (Integer i = 0; i < count; i++) {
|
|
108
|
+
accounts.add(new Account(
|
|
109
|
+
Name = 'Bulk Test Account ' + i,
|
|
110
|
+
Type = 'Customer',
|
|
111
|
+
Customer_Tier__c = 'Standard'
|
|
112
|
+
));
|
|
113
|
+
}
|
|
114
|
+
insert accounts;
|
|
115
|
+
return accounts;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public static User createUserWithProfile(String profileName) {
|
|
119
|
+
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
|
|
120
|
+
User u = new User(
|
|
121
|
+
Alias = 'tstuser',
|
|
122
|
+
Email = generateUniqueString() + '@testfactory.example.com',
|
|
123
|
+
EmailEncodingKey = 'UTF-8',
|
|
124
|
+
LastName = 'Testing',
|
|
125
|
+
LanguageLocaleKey = 'en_US',
|
|
126
|
+
LocaleSidKey = 'en_US',
|
|
127
|
+
ProfileId = p.Id,
|
|
128
|
+
TimeZoneSidKey = 'America/Los_Angeles',
|
|
129
|
+
UserName = generateUniqueString() + '@testfactory.example.com'
|
|
130
|
+
);
|
|
131
|
+
insert u;
|
|
132
|
+
return u;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private static Integer uniqueCounter = 0;
|
|
136
|
+
|
|
137
|
+
private static String generateUniqueString() {
|
|
138
|
+
return String.valueOf(++uniqueCounter) + '_' +
|
|
139
|
+
String.valueOf(Datetime.now().getTime()).right(6);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private static void applyOverrides(SObject record, Map<String, Object> overrides) {
|
|
143
|
+
for (String fieldName : overrides.keySet()) {
|
|
144
|
+
record.put(fieldName, overrides.get(fieldName));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Test Structure: Arrange-Act-Assert
|
|
153
|
+
|
|
154
|
+
Every test method follows three phases with a blank line between them.
|
|
155
|
+
|
|
156
|
+
```apex
|
|
157
|
+
@isTest
|
|
158
|
+
static void testCalculateDiscount_premiumTier_returns20Percent() {
|
|
159
|
+
// Arrange
|
|
160
|
+
Account acc = TestDataFactory.createAccount(
|
|
161
|
+
new Map<String, Object>{ 'Customer_Tier__c' => 'Premium' }
|
|
162
|
+
);
|
|
163
|
+
Decimal orderAmount = 10000;
|
|
164
|
+
|
|
165
|
+
// Act
|
|
166
|
+
Test.startTest();
|
|
167
|
+
Decimal discount = DiscountCalculator.calculate(acc.Id, orderAmount);
|
|
168
|
+
Test.stopTest();
|
|
169
|
+
|
|
170
|
+
// Assert
|
|
171
|
+
Assert.areEqual(2000, discount,
|
|
172
|
+
'Premium tier accounts should receive a 20% discount on $10,000 orders');
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Test Method Naming
|
|
177
|
+
|
|
178
|
+
Format: `test{MethodName}_{scenario}_{expectedResult}`
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
testCalculateDiscount_premiumTier_returns20Percent()
|
|
182
|
+
testCalculateDiscount_nullAmount_returnsZero()
|
|
183
|
+
testCreateAccount_duplicateName_addsFieldError()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Governor Limit Testing
|
|
189
|
+
|
|
190
|
+
Always test with 200 records (standard trigger batch size). A method that works with 1 record may fail at governor limits with 200.
|
|
191
|
+
|
|
192
|
+
```apex
|
|
193
|
+
@isTest
|
|
194
|
+
static void testTrigger_bulkInsert_staysWithinLimits() {
|
|
195
|
+
List<Account> accounts = TestDataFactory.createAccounts(200);
|
|
196
|
+
|
|
197
|
+
List<Account> processed = [
|
|
198
|
+
SELECT Id, Customer_Tier__c FROM Account
|
|
199
|
+
WHERE Id IN :new Map<Id, Account>(accounts).keySet()
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
System.assertEquals(200, processed.size(), 'All 200 accounts should be present');
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Use `Test.startTest()` / `Test.stopTest()` to reset governor limit counters, giving the code under test a fresh limit context.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Exception Testing
|
|
211
|
+
|
|
212
|
+
```apex
|
|
213
|
+
@isTest
|
|
214
|
+
static void testUpgradeToPremium_insufficientRevenue_throwsUpgradeException() {
|
|
215
|
+
Account acc = TestDataFactory.createAccount(
|
|
216
|
+
new Map<String, Object>{ 'AnnualRevenue' => 10000 }
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
Test.startTest();
|
|
220
|
+
try {
|
|
221
|
+
AccountsService.upgradeToPremium(new Set<Id>{ acc.Id });
|
|
222
|
+
Assert.fail('Expected UpgradeException was not thrown');
|
|
223
|
+
} catch (AccountsService.UpgradeException e) {
|
|
224
|
+
Assert.isTrue(
|
|
225
|
+
e.getMessage().contains('Annual revenue must be at least'),
|
|
226
|
+
'Exception message should explain the reason. Got: ' + e.getMessage()
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
Test.stopTest();
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Use the `Assert` class (see @../_reference/API_VERSIONS.md for minimum version): `Assert.areEqual`, `Assert.isTrue`, `Assert.isNotNull`, `Assert.fail`.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Permission Testing with System.runAs
|
|
238
|
+
|
|
239
|
+
```apex
|
|
240
|
+
@isTest
|
|
241
|
+
static void testViewRestrictedReport_standardUser_throwsException() {
|
|
242
|
+
User standardUser = TestDataFactory.createUserWithProfile('Standard User');
|
|
243
|
+
Restricted_Report__c report = new Restricted_Report__c(Name = 'Confidential Q4');
|
|
244
|
+
insert report;
|
|
245
|
+
|
|
246
|
+
Test.startTest();
|
|
247
|
+
System.runAs(standardUser) {
|
|
248
|
+
try {
|
|
249
|
+
ReportService.viewReport(report.Id);
|
|
250
|
+
Assert.fail('Standard user should not be able to view restricted reports');
|
|
251
|
+
} catch (ReportService.AccessDeniedException e) {
|
|
252
|
+
Assert.isTrue(true, 'Expected AccessDeniedException thrown correctly');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
Test.stopTest();
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Async Testing
|
|
262
|
+
|
|
263
|
+
### @future and Queueable
|
|
264
|
+
|
|
265
|
+
`Test.startTest()` / `Test.stopTest()` forces @future and Queueable jobs to execute synchronously.
|
|
266
|
+
|
|
267
|
+
```apex
|
|
268
|
+
@isTest
|
|
269
|
+
static void testFutureCallout_sendsRequest() {
|
|
270
|
+
Test.setMock(HttpCalloutMock.class, new MockERPCallout(200, '{"status":"ok"}'));
|
|
271
|
+
Account acc = TestDataFactory.createAccount();
|
|
272
|
+
|
|
273
|
+
Test.startTest();
|
|
274
|
+
ExternalDataSync.syncAccountToERP(acc.Id);
|
|
275
|
+
Test.stopTest();
|
|
276
|
+
|
|
277
|
+
List<Integration_Error_Log__c> errors = [
|
|
278
|
+
SELECT Id FROM Integration_Error_Log__c WHERE Account__c = :acc.Id
|
|
279
|
+
];
|
|
280
|
+
System.assertEquals(0, errors.size(), 'No errors should be logged');
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Batch Apex
|
|
285
|
+
|
|
286
|
+
```apex
|
|
287
|
+
@isTest
|
|
288
|
+
static void testBatch_processesAllRecords() {
|
|
289
|
+
// insert 200 records
|
|
290
|
+
Test.startTest();
|
|
291
|
+
Database.executeBatch(new AccountAnnualReviewBatch(), 200);
|
|
292
|
+
Test.stopTest(); // start(), execute(), finish() all run synchronously
|
|
293
|
+
// Assert results
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Mock Patterns
|
|
300
|
+
|
|
301
|
+
### HttpCalloutMock
|
|
302
|
+
|
|
303
|
+
```apex
|
|
304
|
+
@isTest
|
|
305
|
+
public class MockERPCallout implements HttpCalloutMock {
|
|
306
|
+
private Integer statusCode;
|
|
307
|
+
private String responseBody;
|
|
308
|
+
|
|
309
|
+
public MockERPCallout(Integer statusCode, String responseBody) {
|
|
310
|
+
this.statusCode = statusCode;
|
|
311
|
+
this.responseBody = responseBody;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
public HttpResponse respond(HttpRequest req) {
|
|
315
|
+
HttpResponse res = new HttpResponse();
|
|
316
|
+
res.setStatusCode(statusCode);
|
|
317
|
+
res.setBody(responseBody);
|
|
318
|
+
res.setHeader('Content-Type', 'application/json');
|
|
319
|
+
return res;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Multi-Callout Mock
|
|
325
|
+
|
|
326
|
+
```apex
|
|
327
|
+
@isTest
|
|
328
|
+
public class MultiCalloutMock implements HttpCalloutMock {
|
|
329
|
+
private Map<String, HttpResponse> responses = new Map<String, HttpResponse>();
|
|
330
|
+
|
|
331
|
+
public MultiCalloutMock addResponse(String urlPattern, Integer statusCode, String body) {
|
|
332
|
+
HttpResponse res = new HttpResponse();
|
|
333
|
+
res.setStatusCode(statusCode);
|
|
334
|
+
res.setBody(body);
|
|
335
|
+
responses.put(urlPattern, res);
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
public HttpResponse respond(HttpRequest req) {
|
|
340
|
+
for (String pattern : responses.keySet()) {
|
|
341
|
+
if (req.getEndpoint().contains(pattern)) {
|
|
342
|
+
return responses.get(pattern);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
throw new CalloutException('No mock response configured for: ' + req.getEndpoint());
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Stub API (System.StubProvider)
|
|
351
|
+
|
|
352
|
+
For mocking dependencies without HTTP, use the `System.StubProvider` interface with `Test.createStub()`.
|
|
353
|
+
|
|
354
|
+
```apex
|
|
355
|
+
IAccountsSelector mockSelector = (IAccountsSelector)
|
|
356
|
+
Test.createStub(IAccountsSelector.class, new MockAccountsSelector(mockAccounts));
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
> **Note:** The instance field must be typed as the interface (e.g., `IAccountsSelector`), not the concrete class. Casting a stub proxy to a concrete class throws TypeException at runtime.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Coverage Strategy
|
|
364
|
+
|
|
365
|
+
Line coverage is misleading. A method with an `if` statement can show 100% line coverage if you only test the `true` branch. Test every branch.
|
|
366
|
+
|
|
367
|
+
```apex
|
|
368
|
+
// This method has 4 branches — test each:
|
|
369
|
+
// 1. testCalculateDiscount_premiumTier
|
|
370
|
+
// 2. testCalculateDiscount_standardTier
|
|
371
|
+
// 3. testCalculateDiscount_unknownTier (else)
|
|
372
|
+
// 4. testCalculateDiscount_nullTier
|
|
373
|
+
public Decimal calculateDiscount(String tier, Decimal amount) {
|
|
374
|
+
if (tier == 'Premium') return amount * 0.20;
|
|
375
|
+
else if (tier == 'Standard') return amount * 0.10;
|
|
376
|
+
else return 0;
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## RunRelevantTests and @testFor (Spring '26)
|
|
383
|
+
|
|
384
|
+
> Requires the minimum API version for this feature (see @../_reference/API_VERSIONS.md). If `sfdx-project.json` specifies a `sourceApiVersion` below it, `RunRelevantTests` silently falls back to `RunLocalTests`.
|
|
385
|
+
|
|
386
|
+
`@testFor` explicitly declares which production class a test class covers, improving `RunRelevantTests` selection accuracy.
|
|
387
|
+
|
|
388
|
+
```apex
|
|
389
|
+
@isTest
|
|
390
|
+
@testFor(AccountService)
|
|
391
|
+
private class AccountServiceTest {
|
|
392
|
+
// tests for AccountService
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**Rules:** Reference any top-level Apex class (not inner classes). Cannot be placed on non-test classes. Use separate test classes for each target — Apex does not support stacking duplicate annotations.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Related
|
|
401
|
+
|
|
402
|
+
- **Agent**: `sf-apex-agent` — For interactive, in-depth guidance
|
|
403
|
+
- **Skills**: `sf-tdd-workflow` — TDD workflow for Apex
|
|
404
|
+
|
|
405
|
+
### Guardrails
|
|
406
|
+
|
|
407
|
+
- `sf-testing-constraints` — Enforces test isolation, assertion requirements, SeeAllData prohibition, and coverage thresholds
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-api-design
|
|
3
|
+
description: >-
|
|
4
|
+
Salesforce API design — custom REST endpoints, batch operations, Composite API, error envelopes, auth. Use when designing APIs exposed from Salesforce. Do NOT use for outbound callouts or Platform Events.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Salesforce API Design
|
|
8
|
+
|
|
9
|
+
Patterns for designing and implementing custom APIs on the Salesforce platform. Callout limits, Composite API limits, and Named Credential details live in the reference file.
|
|
10
|
+
|
|
11
|
+
@../_reference/INTEGRATION_PATTERNS.md
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- Designing custom REST or SOAP APIs exposed from Salesforce using Apex
|
|
16
|
+
- Establishing consistent API response envelopes and error handling patterns
|
|
17
|
+
- Configuring authentication for inbound API access
|
|
18
|
+
- Building batch REST endpoints for bulk operations
|
|
19
|
+
- Reviewing Apex API code for security (CRUD/FLS, input validation, status codes)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Custom REST API Pattern
|
|
24
|
+
|
|
25
|
+
```apex
|
|
26
|
+
@RestResource(urlMapping='/api/accounts/*')
|
|
27
|
+
global with sharing class AccountAPI {
|
|
28
|
+
|
|
29
|
+
@HttpGet
|
|
30
|
+
global static void getAccount() {
|
|
31
|
+
RestRequest req = RestContext.request;
|
|
32
|
+
RestResponse res = RestContext.response;
|
|
33
|
+
|
|
34
|
+
String accountId = req.requestURI.substringAfterLast('/');
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
Account acc = [
|
|
38
|
+
SELECT Id, Name, Industry, AnnualRevenue
|
|
39
|
+
FROM Account WHERE Id = :accountId
|
|
40
|
+
WITH USER_MODE LIMIT 1
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
res.statusCode = 200;
|
|
44
|
+
res.responseBody = Blob.valueOf(JSON.serialize(
|
|
45
|
+
new ApiResponse(true, acc, null)));
|
|
46
|
+
} catch (QueryException e) {
|
|
47
|
+
res.statusCode = 404;
|
|
48
|
+
res.responseBody = Blob.valueOf(JSON.serialize(
|
|
49
|
+
new ApiResponse(false, null, 'Account not found')));
|
|
50
|
+
} catch (Exception e) {
|
|
51
|
+
res.statusCode = 500;
|
|
52
|
+
res.responseBody = Blob.valueOf(JSON.serialize(
|
|
53
|
+
new ApiResponse(false, null, 'An internal error occurred')));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@HttpPost
|
|
58
|
+
global static void createAccount() {
|
|
59
|
+
RestRequest req = RestContext.request;
|
|
60
|
+
RestResponse res = RestContext.response;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
Object parsed = JSON.deserializeUntyped(req.requestBody.toString());
|
|
64
|
+
if (!(parsed instanceof Map<String, Object>)) {
|
|
65
|
+
res.statusCode = 400;
|
|
66
|
+
res.responseBody = Blob.valueOf(JSON.serialize(
|
|
67
|
+
new ApiResponse(false, null,
|
|
68
|
+
'Expected JSON object, got ' +
|
|
69
|
+
(parsed instanceof List<Object> ? 'array' : 'primitive')
|
|
70
|
+
)));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
Map<String, Object> body = (Map<String, Object>) parsed;
|
|
74
|
+
Account acc = new Account(
|
|
75
|
+
Name = (String) body.get('name'),
|
|
76
|
+
Industry = (String) body.get('industry')
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// stripInaccessible — check getRemovedFields() to avoid silent data loss
|
|
80
|
+
SObjectAccessDecision decision = Security.stripInaccessible(
|
|
81
|
+
AccessType.CREATABLE, new List<Account>{acc});
|
|
82
|
+
if (!decision.getRemovedFields().isEmpty()) {
|
|
83
|
+
res.statusCode = 403;
|
|
84
|
+
res.responseBody = Blob.valueOf(JSON.serialize(
|
|
85
|
+
new ApiResponse(false, null,
|
|
86
|
+
'Insufficient field permissions for: ' +
|
|
87
|
+
decision.getRemovedFields())));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
insert decision.getRecords();
|
|
91
|
+
|
|
92
|
+
res.statusCode = 201;
|
|
93
|
+
res.responseBody = Blob.valueOf(JSON.serialize(
|
|
94
|
+
new ApiResponse(true, decision.getRecords()[0], null)));
|
|
95
|
+
} catch (Exception e) {
|
|
96
|
+
res.statusCode = 400;
|
|
97
|
+
res.responseBody = Blob.valueOf(JSON.serialize(
|
|
98
|
+
new ApiResponse(false, null, e.getMessage())));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
global class ApiResponse {
|
|
103
|
+
public Boolean success;
|
|
104
|
+
public Object data;
|
|
105
|
+
public String error;
|
|
106
|
+
|
|
107
|
+
public ApiResponse(Boolean success, Object data, String error) {
|
|
108
|
+
this.success = success;
|
|
109
|
+
this.data = data;
|
|
110
|
+
this.error = error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Batch REST Endpoint Pattern
|
|
119
|
+
|
|
120
|
+
```apex
|
|
121
|
+
@HttpPost
|
|
122
|
+
global static void bulkCreate() {
|
|
123
|
+
RestRequest req = RestContext.request;
|
|
124
|
+
RestResponse res = RestContext.response;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
List<Object> items = (List<Object>) JSON.deserializeUntyped(
|
|
128
|
+
req.requestBody.toString());
|
|
129
|
+
List<Account> accounts = new List<Account>();
|
|
130
|
+
|
|
131
|
+
for (Object item : items) {
|
|
132
|
+
Map<String, Object> fields = (Map<String, Object>) item;
|
|
133
|
+
accounts.add(new Account(
|
|
134
|
+
Name = (String) fields.get('name'),
|
|
135
|
+
Industry = (String) fields.get('industry')
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
SObjectAccessDecision decision = Security.stripInaccessible(
|
|
140
|
+
AccessType.CREATABLE, accounts);
|
|
141
|
+
List<Database.SaveResult> results =
|
|
142
|
+
Database.insert(decision.getRecords(), false);
|
|
143
|
+
|
|
144
|
+
List<Object> response = new List<Object>();
|
|
145
|
+
for (Integer i = 0; i < results.size(); i++) {
|
|
146
|
+
Map<String, Object> row = new Map<String, Object>();
|
|
147
|
+
row.put('index', i);
|
|
148
|
+
row.put('success', results[i].isSuccess());
|
|
149
|
+
row.put('id', results[i].isSuccess() ? results[i].getId() : null);
|
|
150
|
+
if (!results[i].isSuccess()) {
|
|
151
|
+
row.put('errors', results[i].getErrors()[0].getMessage());
|
|
152
|
+
}
|
|
153
|
+
response.add(row);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
res.statusCode = 200;
|
|
157
|
+
res.responseBody = Blob.valueOf(JSON.serialize(
|
|
158
|
+
new ApiResponse(true, response, null)));
|
|
159
|
+
} catch (Exception e) {
|
|
160
|
+
res.statusCode = 400;
|
|
161
|
+
res.responseBody = Blob.valueOf(JSON.serialize(
|
|
162
|
+
new ApiResponse(false, null, e.getMessage())));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Error Handling Patterns
|
|
170
|
+
|
|
171
|
+
Structured error codes for API consumers:
|
|
172
|
+
|
|
173
|
+
```apex
|
|
174
|
+
global class ApiError {
|
|
175
|
+
public String code;
|
|
176
|
+
public String message;
|
|
177
|
+
public String field;
|
|
178
|
+
|
|
179
|
+
public ApiError(String code, String message, String field) {
|
|
180
|
+
this.code = code;
|
|
181
|
+
this.message = message;
|
|
182
|
+
this.field = field;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Standard error codes:
|
|
187
|
+
// FIELD_REQUIRED — Missing required field
|
|
188
|
+
// RECORD_NOT_FOUND — Record ID doesn't exist or no access
|
|
189
|
+
// GOVERNOR_LIMIT — Operation would exceed governor limits
|
|
190
|
+
// INSUFFICIENT_ACCESS — User lacks CRUD/FLS permission
|
|
191
|
+
// VALIDATION_FAILED — Validation rule or trigger prevented save
|
|
192
|
+
// DUPLICATE_VALUE — Unique field constraint violated
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Authentication for Inbound APIs
|
|
198
|
+
|
|
199
|
+
| Method | Use When | Setup |
|
|
200
|
+
|--------|----------|-------|
|
|
201
|
+
| Named Principal | All API users share one Salesforce user | Connected App + single auth |
|
|
202
|
+
| Per-User | Each API caller maps to a Salesforce user | Connected App + OAuth per user |
|
|
203
|
+
| JWT Bearer | Server-to-server, no user interaction | Connected App + X.509 certificate |
|
|
204
|
+
| API Key (Custom) | Simple external tools | Custom Metadata + header validation |
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Anti-Patterns
|
|
209
|
+
|
|
210
|
+
| Anti-Pattern | Problem | Fix |
|
|
211
|
+
|-------------|---------|-----|
|
|
212
|
+
| God endpoint (all CRUD in one method) | Hard to maintain and test | One method per operation (@HttpGet, @HttpPost) |
|
|
213
|
+
| No pagination | Timeouts, governor limits | Add LIMIT + OFFSET or cursor-based pagination |
|
|
214
|
+
| Exposing internal Salesforce IDs | Security risk, breaks across orgs | Use external IDs or custom identifiers |
|
|
215
|
+
| No error codes | Consumers can't programmatically handle errors | Return structured error codes |
|
|
216
|
+
| No API versioning | Breaking changes affect all consumers | Version via URL path: `/api/v1/accounts/` |
|
|
217
|
+
| `WITHOUT SHARING` on API class | Bypasses record-level security | Use `WITH SHARING` on REST resources |
|
|
218
|
+
| Returning all fields | Wastes bandwidth, exposes sensitive data | Return only requested/needed fields |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Best Practices
|
|
223
|
+
|
|
224
|
+
- Use `WITH USER_MODE` in SOQL and `AccessLevel.USER_MODE` in DML
|
|
225
|
+
- Use `Security.stripInaccessible()` when you need field-level enforcement on DML -- check `getRemovedFields()` for critical fields
|
|
226
|
+
- Return consistent response envelopes (success, data, error)
|
|
227
|
+
- Use proper HTTP status codes (200, 201, 400, 404, 500)
|
|
228
|
+
- Implement rate limiting awareness (API request limits)
|
|
229
|
+
- Version APIs via URL path (`/api/v1/accounts/`)
|
|
230
|
+
- Use `Database.insert(records, false)` for bulk APIs to support partial success
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Related
|
|
235
|
+
|
|
236
|
+
- Constraints: sf-security-constraints
|
|
237
|
+
- Reference: @../_reference/INTEGRATION_PATTERNS.md
|