@sfdxy/mule-lint 1.21.0 → 1.23.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.
Files changed (37) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/mcp/prompts/index.d.ts +1 -1
  3. package/dist/src/mcp/prompts/index.d.ts.map +1 -1
  4. package/dist/src/mcp/prompts/index.js +62 -1
  5. package/dist/src/mcp/prompts/index.js.map +1 -1
  6. package/dist/src/mcp/resources/index.js +114 -16
  7. package/dist/src/mcp/resources/index.js.map +1 -1
  8. package/dist/src/mcp/tools/getRuleDetails.d.ts.map +1 -1
  9. package/dist/src/mcp/tools/getRuleDetails.js +30 -1
  10. package/dist/src/mcp/tools/getRuleDetails.js.map +1 -1
  11. package/dist/src/rules/index.js +3 -3
  12. package/dist/src/rules/index.js.map +1 -1
  13. package/dist/src/rules/logging/{NewLoggingRules.d.ts → LoggingPatternRules.d.ts} +1 -1
  14. package/dist/src/rules/logging/LoggingPatternRules.d.ts.map +1 -0
  15. package/dist/src/rules/logging/{NewLoggingRules.js → LoggingPatternRules.js} +1 -1
  16. package/dist/src/rules/logging/LoggingPatternRules.js.map +1 -0
  17. package/dist/src/rules/naming/FlowCasingRule.d.ts +6 -3
  18. package/dist/src/rules/naming/FlowCasingRule.d.ts.map +1 -1
  19. package/dist/src/rules/naming/FlowCasingRule.js +26 -8
  20. package/dist/src/rules/naming/FlowCasingRule.js.map +1 -1
  21. package/docs/README.md +87 -27
  22. package/docs/best-practices/ci-cd.md +135 -0
  23. package/docs/best-practices/connector-patterns.md +253 -0
  24. package/docs/best-practices/dataweave-patterns.md +370 -0
  25. package/docs/best-practices/deployment-2026.md +171 -0
  26. package/docs/best-practices/error-handling.md +277 -0
  27. package/docs/best-practices/event-driven-patterns.md +424 -0
  28. package/docs/best-practices/logging.md +163 -0
  29. package/docs/best-practices/mulesoft-best-practices.md +72 -865
  30. package/docs/best-practices/performance.md +273 -0
  31. package/docs/best-practices/security.md +181 -0
  32. package/docs/best-practices/testing.md +190 -0
  33. package/docs/best-practices/variable-contracts.md +191 -0
  34. package/docs/linter/naming-conventions.md +4 -0
  35. package/package.json +1 -1
  36. package/dist/src/rules/logging/NewLoggingRules.d.ts.map +0 -1
  37. package/dist/src/rules/logging/NewLoggingRules.js.map +0 -1
@@ -0,0 +1,273 @@
1
+ # Performance Best Practices
2
+
3
+ > **Applies to:** All
4
+ > **Related Rules:** `MULE-501` · `MULE-502` · `MULE-503` · `PERF-002` · `RES-001` · `RES-002` · `MULE-801`
5
+ > **Last Updated:** April 2026
6
+
7
+ ## When to Read This
8
+
9
+ Read this when optimizing flow performance, configuring connection pooling, implementing async patterns, or reviewing a project for production readiness.
10
+
11
+ ---
12
+
13
+ ## Key Principles
14
+
15
+ 1. **Set explicit timeouts** — avoid hanging connections and resource leaks
16
+ 2. **Handle async errors** — async scopes silently swallow errors
17
+ 3. **Limit choice complexity** — refactor large choice blocks to DWL lookups
18
+ 4. **Configure connection pools** — essential for production throughput
19
+ 5. **Use streaming for large payloads** — avoid loading everything into memory
20
+
21
+ ---
22
+
23
+ ## Patterns
24
+
25
+ ### Pattern 1: Async Scope Error Handling
26
+
27
+ Async scopes don't propagate errors to the parent flow — they're silently lost:
28
+
29
+ ```xml
30
+ <!-- ❌ Bad — errors silently swallowed -->
31
+ <async>
32
+ <http:request config-ref="HTTP_Config" path="/webhook"/>
33
+ </async>
34
+
35
+ <!-- ✅ Good — explicit error handling inside async -->
36
+ <async>
37
+ <try>
38
+ <http:request config-ref="HTTP_Config" path="/webhook"/>
39
+ <error-handler>
40
+ <on-error-continue>
41
+ <logger category="com.myorg.async" level="ERROR"
42
+ message="#['Async webhook failed: ' ++ error.description]"/>
43
+ </on-error-continue>
44
+ </error-handler>
45
+ </try>
46
+ </async>
47
+ ```
48
+
49
+ ### Pattern 2: Refactor Large Choice Blocks
50
+
51
+ ```xml
52
+ <!-- ❌ Bad — too many when clauses (> 5) -->
53
+ <choice>
54
+ <when expression="#[vars.status == 'NEW']">...</when>
55
+ <when expression="#[vars.status == 'PENDING']">...</when>
56
+ <when expression="#[vars.status == 'APPROVED']">...</when>
57
+ <!-- 10+ more conditions -->
58
+ <otherwise>...</otherwise>
59
+ </choice>
60
+
61
+ <!-- ✅ Good — DataWeave lookup + dynamic flow-ref -->
62
+ <ee:transform>
63
+ <ee:set-variable variableName="handler"><![CDATA[%dw 2.0
64
+ var handlers = {
65
+ "NEW": "new-handler-subflow",
66
+ "PENDING": "pending-handler-subflow",
67
+ "APPROVED": "approved-handler-subflow"
68
+ }
69
+ ---
70
+ handlers[vars.status] default "default-handler-subflow"
71
+ ]]></ee:set-variable>
72
+ </ee:transform>
73
+ <flow-ref name="#[vars.handler]"/>
74
+ ```
75
+
76
+ ### Pattern 3: HTTP Timeout Configuration
77
+
78
+ ```xml
79
+ <!-- ✅ Always set explicit timeouts -->
80
+ <http:request-config name="API_Config"
81
+ responseTimeout="${https.request.responseTimeout}">
82
+ <http:request-connection host="${api.host}" port="${api.port}">
83
+ <http:client-socket-properties>
84
+ <http:tcp-client-socket-properties connectionTimeout="30000"/>
85
+ </http:client-socket-properties>
86
+ </http:request-connection>
87
+ </http:request-config>
88
+ ```
89
+
90
+ > ⚠️ **Grizzly rule:** `connectionIdleTimeout` must be ≥ `responseTimeout`. If idle timeout fires first, it kills the connection before the response arrives.
91
+
92
+ ### Pattern 4: Connection Pooling
93
+
94
+ ```xml
95
+ <!-- ✅ Good — HTTP with pooling configured -->
96
+ <http:request-config name="API_Config">
97
+ <http:request-connection host="${api.host}" port="${api.port}"
98
+ maxConnections="20"
99
+ connectionIdleTimeout="300000"/>
100
+ </http:request-config>
101
+
102
+ <!-- ✅ Good — Database with pooling -->
103
+ <db:config name="Database_Config">
104
+ <db:my-sql-connection host="${db.host}" port="${db.port}"
105
+ database="${db.name}" user="${db.user}"
106
+ password="${secure::db.password}">
107
+ <db:pooling-profile maxPoolSize="10"
108
+ minPoolSize="2" maxWait="30000"/>
109
+ </db:my-sql-connection>
110
+ </db:config>
111
+ ```
112
+
113
+ ### Pattern 5: Scatter-Gather Limits
114
+
115
+ Keep parallel routes manageable (< 5–7 routes):
116
+
117
+ ```xml
118
+ <scatter-gather>
119
+ <route><flow-ref name="service1-subflow"/></route>
120
+ <route><flow-ref name="service2-subflow"/></route>
121
+ <route><flow-ref name="service3-subflow"/></route>
122
+ </scatter-gather>
123
+ ```
124
+
125
+ ### Pattern 6: Streaming for Large Payloads
126
+
127
+ Mule 4 uses **file-stored repeatable streams** by default (512 KB in-memory buffer). For large payloads:
128
+
129
+ - **Don't** call `payload` multiple times — it forces stream re-read
130
+ - **Do** store needed values in variables before consuming the stream
131
+ - **Do** use streaming-compatible connectors and DataWeave
132
+
133
+ ```xml
134
+ <!-- ✅ Store needed values before processing -->
135
+ <set-variable variableName="recordCount" value="#[sizeOf(payload)]"/>
136
+ <ee:transform>
137
+ <ee:set-payload resource="dwl/transforms/process-batch.dwl"/>
138
+ </ee:transform>
139
+ ```
140
+
141
+ ### Pattern 7: Pre-batch Bulk Lookup (N+1 Prevention)
142
+
143
+ **Use when:** iterating over 10+ records with `foreach` where each record requires 1+ related record lookups from an external system. Without this pattern, 100 records × 3 lookups = 300 API calls. With it: 3 API calls.
144
+
145
+ **Do NOT use when:** iterating < 5 records (overhead not worth it), lookups are already cached in ObjectStore, or the downstream system doesn't support bulk/IN queries.
146
+
147
+ | Scenario | Bulk Lookup? | Why |
148
+ | ------------------------------------------ | ------------ | ------------------------ |
149
+ | 100 records, each needs 3 related lookups | ✅ | 300 calls → 3 calls |
150
+ | 3 records, each needs 1 lookup | ❌ | Overhead exceeds savings |
151
+ | Lookups already cached in ObjectStore | ❌ | Already optimized |
152
+ | Downstream doesn't support IN/bulk queries | ❌ | Can't batch anyway |
153
+
154
+ **Architecture:**
155
+
156
+ ```
157
+ 1. Save batch to variable
158
+ 2. Extract unique lookup IDs (distinctBy, filter !isEmpty)
159
+ 3. Scatter-gather: bulk-fetch all related records in parallel
160
+ 4. Build groupBy lookup maps → O(1) resolution per record
161
+ 5. Restore original batch as payload
162
+ 6. Foreach: resolve from maps (zero API calls)
163
+ ```
164
+
165
+ **Implementation:**
166
+
167
+ ```xml
168
+ <sub-flow name="bulkResolveLookupsSubFlow">
169
+ <!-- 1. Save the original batch -->
170
+ <set-variable value="#[payload]" variableName="batch"/>
171
+
172
+ <!-- 2. Extract unique lookup IDs -->
173
+ <ee:transform>
174
+ <ee:variables>
175
+ <ee:set-variable variableName="lookupIds"><![CDATA[%dw 2.0
176
+ output application/java
177
+ ---
178
+ {
179
+ projectIds: (vars.batch.projectId default [])
180
+ filter (!isEmpty($)) distinctBy $,
181
+ contactIds: (vars.batch.contactId default [])
182
+ filter (!isEmpty($)) distinctBy $
183
+ }]]></ee:set-variable>
184
+ </ee:variables>
185
+ </ee:transform>
186
+
187
+ <!-- 3. Bulk fetch in parallel -->
188
+ <scatter-gather>
189
+ <route>
190
+ <http:request method="GET" path="/Project__c">
191
+ <http:query-params>#[output application/java --- {
192
+ "query": "SELECT Id, Project_ID__c FROM Project__c
193
+ WHERE Project_ID__c IN ('"
194
+ ++ (vars.lookupIds.projectIds joinBy "','") ++ "')"
195
+ }]</http:query-params>
196
+ </http:request>
197
+ </route>
198
+ <route>
199
+ <http:request method="GET" path="/Contact">
200
+ <http:query-params>#[output application/java --- {
201
+ "query": "SELECT Id, AccountId FROM Contact
202
+ WHERE Id IN ('"
203
+ ++ (vars.lookupIds.contactIds joinBy "','") ++ "')"
204
+ }]</http:query-params>
205
+ </http:request>
206
+ </route>
207
+ </scatter-gather>
208
+
209
+ <!-- 4. Build lookup maps with groupBy -->
210
+ <ee:transform>
211
+ <ee:variables>
212
+ <ee:set-variable variableName="lookupMaps"><![CDATA[%dw 2.0
213
+ output application/java
214
+ ---
215
+ {
216
+ projects: (payload.'0'.payload default []) groupBy $.Project_ID__c,
217
+ contacts: (payload.'1'.payload default []) groupBy $.Id
218
+ }]]></ee:set-variable>
219
+ </ee:variables>
220
+ </ee:transform>
221
+
222
+ <!-- 5. Restore original batch (scatter-gather replaced payload) -->
223
+ <set-payload value="#[vars.batch]"/>
224
+ </sub-flow>
225
+ ```
226
+
227
+ **Then in the foreach — zero API calls per iteration:**
228
+
229
+ ```dataweave
230
+ var resolvedProject = (vars.lookupMaps.projects[payload.projectId] default [])[0]
231
+ ---
232
+ {
233
+ message: payload ++ { resolvedProject: resolvedProject },
234
+ recordType: "scheduled-entity"
235
+ }
236
+ ```
237
+
238
+ **Key rules:**
239
+
240
+ - **Always save-and-restore payload** — `scatter-gather` replaces `payload` with its own result object
241
+ - **`distinctBy $`** — deduplicate IDs (same project may appear on 50 records)
242
+ - **`filter !isEmpty($)`** — exclude null/empty IDs before building the IN clause
243
+ - **`groupBy` for O(1) resolution** — returns an array; take `[0]` for the first match
244
+ - **Guard the scatter-gather** — if no IDs exist, skip the bulk fetch entirely (set empty maps)
245
+
246
+ ---
247
+
248
+ ## Flow Complexity Guidelines
249
+
250
+ | Metric | Threshold | Action |
251
+ | --------------------- | ---------- | ----------------------- |
252
+ | Processors per flow | ≤ 15 | Split into sub-flows |
253
+ | Choice branches | ≤ 5 | Use DWL lookup pattern |
254
+ | Scatter-gather routes | ≤ 7 | Consolidate or sequence |
255
+ | Flow-ref depth | ≤ 5 levels | Flatten chain |
256
+ | Loggers per flow | ≤ 5 | Move to DEBUG level |
257
+
258
+ ---
259
+
260
+ ## Checklist
261
+
262
+ - [ ] `responseTimeout` set on all HTTP request configs
263
+ - [ ] `connectionIdleTimeout >= responseTimeout` (Grizzly rule)
264
+ - [ ] Connection pooling configured for HTTP and Database connectors
265
+ - [ ] Async scopes have internal error handling
266
+ - [ ] Choice blocks have ≤ 5 branches (refactor large ones)
267
+ - [ ] Scatter-gather has ≤ 7 routes
268
+ - [ ] Reconnection strategies on all outbound connectors
269
+ - [ ] Pre-batch bulk lookup used for foreach with 10+ records needing related lookups (Pattern 7)
270
+
271
+ ---
272
+
273
+ **See also:** [Connector Patterns](connector-patterns.md) · [Error Handling](error-handling.md)
@@ -0,0 +1,181 @@
1
+ # Security Best Practices
2
+
3
+ > **Applies to:** All
4
+ > **Related Rules:** `MULE-004` · `MULE-201` · `MULE-202` · `SEC-002` – `SEC-010` · `YAML-004` · `LOG-004`
5
+ > **Last Updated:** April 2026
6
+
7
+ ## When to Read This
8
+
9
+ Read this when handling credentials, configuring TLS, securing API endpoints, or reviewing a project for security compliance.
10
+
11
+ ---
12
+
13
+ ## Key Principles
14
+
15
+ 1. **Never hardcode secrets** — use `${secure::...}` property placeholders
16
+ 2. **Never hardcode URLs** — all hosts, ports, and paths go in YAML config
17
+ 3. **Encrypt sensitive properties** — AES/CBC with externalized key
18
+ 4. **Use TLS 1.2+ only** — TLS 1.0/1.1 and SSLv3 are deprecated
19
+ 5. **Shift-left security** — validate security in design phase, not after deployment
20
+
21
+ ---
22
+
23
+ ## Patterns
24
+
25
+ ### Pattern 1: Secure Properties Configuration
26
+
27
+ ```xml
28
+ <!-- Load secure properties with externalized encryption key -->
29
+ <secure-properties:config name="Secure_Properties_Config"
30
+ file="config/secure-${mule.env}.yaml"
31
+ key="${secure.key}">
32
+ <secure-properties:encrypt algorithm="AES" mode="CBC"/>
33
+ </secure-properties:config>
34
+ ```
35
+
36
+ ```yaml
37
+ # config/secure-dev.yaml (encrypted values)
38
+ salesforce:
39
+ jwt:
40
+ consumerKey: '![encrypted-value]'
41
+ storePassword: '![encrypted-value]'
42
+ ```
43
+
44
+ **Rules:**
45
+
46
+ - `key` attribute MUST be a property placeholder (`${secure.key}`) — never hardcoded
47
+ - Use `AES` or `Blowfish` — `DES` is considered weak
48
+ - Encrypt using Anypoint Secure Properties Tool with runtime property `secure.key`
49
+
50
+ ### Pattern 2: Connector Credential Security
51
+
52
+ ```xml
53
+ <!-- ❌ Bad — plaintext credentials -->
54
+ <http:request-config username="admin" password="secret123"/>
55
+
56
+ <!-- ❌ Bad — non-secure property -->
57
+ <http:request-config username="${api.username}" password="${api.password}"/>
58
+
59
+ <!-- ✅ Good — secure property reference -->
60
+ <http:request-config
61
+ username="${api.username}"
62
+ password="${secure::api.password}"/>
63
+ ```
64
+
65
+ Credential attributes that must use `${secure::...}`:
66
+
67
+ - `password`, `storePassword`
68
+ - `clientId`, `clientSecret`, `consumerKey`, `consumerSecret`
69
+ - `token`, `tokenSecret`, `tokenId`
70
+
71
+ ### Pattern 3: TLS Configuration
72
+
73
+ ```xml
74
+ <!-- ❌ Bad — insecure TLS -->
75
+ <tls:context name="Insecure_TLS">
76
+ <tls:trust-store insecure="true"/>
77
+ </tls:context>
78
+
79
+ <!-- ❌ Bad — deprecated protocol -->
80
+ <tls:context enabledProtocols="TLSv1.1,TLSv1.2">
81
+
82
+ <!-- ✅ Good — proper certificate validation -->
83
+ <tls:context name="Secure_TLS" enabledProtocols="TLSv1.2,TLSv1.3">
84
+ <tls:trust-store path="${tls.truststore.path}"
85
+ password="${secure::tls.truststore.password}"/>
86
+ </tls:context>
87
+ ```
88
+
89
+ ### Pattern 4: Never Log Sensitive Data
90
+
91
+ ```xml
92
+ <!-- ❌ Bad — logs sensitive values -->
93
+ <logger message="#['Token: ' ++ vars.accessToken]"/>
94
+ <logger message="#[payload]"/> <!-- May contain PII -->
95
+
96
+ <!-- ✅ Good — logs only identifiers -->
97
+ <logger message="#['Auth successful for user: ' ++ vars.userId]"/>
98
+ <logger message="#['Processing order: ' ++ payload.orderId]"/>
99
+ ```
100
+
101
+ **Detected patterns** (flagged by `SEC-006` and `LOG-004`):
102
+ `encrypt.*key`, `password`, `credentials`, `api_key`, `secret.*key`, `accessToken`, `SSN`, `creditCard`
103
+
104
+ ### Pattern 5: API Rate Limiting
105
+
106
+ For CloudHub-deployed APIs, rate limiting is applied via **API Manager policies** — not inline XML:
107
+
108
+ ```yaml
109
+ # dev.yaml — populate for API Manager autodiscovery
110
+ api:
111
+ id: '12345678' # API Manager API instance ID
112
+ ```
113
+
114
+ For self-managed deployments, configure inline:
115
+
116
+ ```xml
117
+ <throttling:config name="Rate_Limit_Config" maxRequestsPerPeriod="100" timePeriodInMilliseconds="60000"/>
118
+ ```
119
+
120
+ ### Pattern 6: Input Validation
121
+
122
+ ```xml
123
+ <!-- ✅ Good — validate incoming payload against schema -->
124
+ <flow name="post:\orders:api-config">
125
+ <json:validate-schema schema="schemas/order-request.json"/>
126
+ <!-- Process only after validation passes -->
127
+ </flow>
128
+ ```
129
+
130
+ For APIs using APIKit with RAML, schema validation is handled by the APIKit router based on the RAML type definitions.
131
+
132
+ ---
133
+
134
+ ## Zero-Trust API Architecture (2026+)
135
+
136
+ Modern MuleSoft deployments follow Zero-Trust principles:
137
+
138
+ | Layer | Control | Implementation |
139
+ | ----------------- | ------------------------------------ | ---------------------------------------- |
140
+ | **Network** | mTLS between services | TLS context with client certificates |
141
+ | **Identity** | OAuth 2.0 / JWT verified per request | API Manager policies + Flex Gateway |
142
+ | **Authorization** | Scoped permissions per API consumer | Client ID enforcement + scope validation |
143
+ | **Data** | Encrypt at rest and in transit | Secure properties + TLS 1.3 |
144
+ | **Observability** | Audit all access | Correlation IDs + structured logging |
145
+
146
+ ---
147
+
148
+ ## Environment Property Separation
149
+
150
+ ```
151
+ config/
152
+ ├── global.yaml # Shared defaults (timeouts, paths)
153
+ ├── dev.yaml # Dev-specific non-sensitive config
154
+ ├── prod.yaml # Prod-specific non-sensitive config
155
+ ├── secure-dev.yaml # Dev encrypted credentials
156
+ └── secure-prod.yaml # Prod encrypted credentials
157
+ ```
158
+
159
+ **Never store:**
160
+
161
+ - Plaintext passwords in any YAML file
162
+ - API keys or tokens in non-secure YAML files
163
+ - Encryption keys inline in XML
164
+
165
+ ---
166
+
167
+ ## Checklist
168
+
169
+ - [ ] All credentials use `${secure::...}` property placeholders
170
+ - [ ] Secure properties encryption key is externalized (`${secure.key}`)
171
+ - [ ] Encryption algorithm is AES or Blowfish (not DES)
172
+ - [ ] TLS contexts use TLSv1.2+ only — no SSLv3, TLSv1.0, TLSv1.1
173
+ - [ ] No `insecure="true"` on trust-stores
174
+ - [ ] No hardcoded URLs, hosts, or ports in XML
175
+ - [ ] No sensitive data in logger messages
176
+ - [ ] Input validation configured for all POST/PUT/PATCH endpoints
177
+ - [ ] API Manager `api.id` populated in environment YAML
178
+
179
+ ---
180
+
181
+ **See also:** [Connector Patterns](connector-patterns.md) · [Logging](logging.md) · [Configuration Management](variable-contracts.md)
@@ -0,0 +1,190 @@
1
+ # Testing with MUnit
2
+
3
+ > **Applies to:** All
4
+ > **Related Rules:** `EXP-003`
5
+ > **Last Updated:** April 2026
6
+
7
+ ## When to Read This
8
+
9
+ Read this when writing MUnit tests, setting up test infrastructure, or testing event-driven flows.
10
+
11
+ ---
12
+
13
+ ## Test Coverage Goals
14
+
15
+ | Test Type | Coverage Target | Purpose |
16
+ | -------------------- | ------------------ | ------------------------------ |
17
+ | Unit Tests | 80%+ flow coverage | Validate individual flow logic |
18
+ | Integration Tests | All critical paths | Validate end-to-end scenarios |
19
+ | Error Scenario Tests | All error handlers | Validate error responses |
20
+
21
+ ---
22
+
23
+ ## Patterns
24
+
25
+ ### Pattern 1: Standard MUnit Test Structure
26
+
27
+ ```xml
28
+ <munit:test name="create-order-success-test"
29
+ description="Validates successful order creation">
30
+
31
+ <!-- Mock external dependencies -->
32
+ <munit:behavior>
33
+ <munit-tools:mock-when processor="http:request">
34
+ <munit-tools:with-attributes>
35
+ <munit-tools:with-attribute attributeName="config-ref"
36
+ whereValue="API_HTTP_Config"/>
37
+ </munit-tools:with-attributes>
38
+ <munit-tools:then-return>
39
+ <munit-tools:payload value='{"orderId": "12345"}'/>
40
+ </munit-tools:then-return>
41
+ </munit-tools:mock-when>
42
+ </munit:behavior>
43
+
44
+ <!-- Execute the flow -->
45
+ <munit:execution>
46
+ <flow-ref name="create-order-flow"/>
47
+ </munit:execution>
48
+
49
+ <!-- Assert results -->
50
+ <munit:validation>
51
+ <munit-tools:assert-that expression="#[payload.orderId]"
52
+ is="#[MunitTools::notNullValue()]"/>
53
+ </munit:validation>
54
+ </munit:test>
55
+ ```
56
+
57
+ ### Pattern 2: Testing Error Handlers
58
+
59
+ ```xml
60
+ <munit:test name="error-handler-400-test"
61
+ description="Validates 400 Bad Request error handling"
62
+ expectedErrorType="APIKIT:BAD_REQUEST">
63
+
64
+ <munit:behavior>
65
+ <munit-tools:mock-when processor="apikit:router">
66
+ <munit-tools:then-call exception="APIKIT:BAD_REQUEST"/>
67
+ </munit-tools:mock-when>
68
+ </munit:behavior>
69
+
70
+ <munit:execution>
71
+ <flow-ref name="api-main"/>
72
+ </munit:execution>
73
+
74
+ <munit:validation>
75
+ <munit-tools:assert-that expression="#[vars.httpStatus]"
76
+ is="#[MunitTools::equalTo(400)]"/>
77
+ <munit-tools:assert-that expression="#[payload.error]"
78
+ is="#[MunitTools::equalTo('InvalidInput')]"/>
79
+ </munit:validation>
80
+ </munit:test>
81
+ ```
82
+
83
+ ### Pattern 3: Testing Event-Driven Flows
84
+
85
+ For PAPI-style flows driven by Platform Events, mock the event payload and downstream SAPI calls:
86
+
87
+ ```xml
88
+ <munit:test name="account-event-processing-test"
89
+ description="Validates Account event → NS Customer sync">
90
+
91
+ <munit:behavior>
92
+ <!-- Mock the SAPI HTTP call -->
93
+ <munit-tools:mock-when processor="http:request">
94
+ <munit-tools:with-attributes>
95
+ <munit-tools:with-attribute attributeName="config-ref"
96
+ whereValue="SAPI_HTTP_Config"/>
97
+ </munit-tools:with-attributes>
98
+ <munit-tools:then-return>
99
+ <munit-tools:payload value='{"status":"success","internalId":"123"}'/>
100
+ </munit-tools:then-return>
101
+ </munit-tools:mock-when>
102
+
103
+ <!-- Mock the writeback SAPI call -->
104
+ <munit-tools:mock-when processor="http:request">
105
+ <munit-tools:with-attributes>
106
+ <munit-tools:with-attribute attributeName="doc:name"
107
+ whereValue="Writeback Request"/>
108
+ </munit-tools:with-attributes>
109
+ <munit-tools:then-return>
110
+ <munit-tools:payload value='{"success":true}'/>
111
+ </munit-tools:then-return>
112
+ </munit-tools:mock-when>
113
+ </munit:behavior>
114
+
115
+ <munit:execution>
116
+ <!-- Set variables as the event listener would -->
117
+ <set-variable variableName="correlationId" value="test-uuid-123"/>
118
+ <set-variable variableName="logCategory" value="com.myorg.papi"/>
119
+ <set-variable variableName="salesforceId" value="001XXXXX"/>
120
+ <set-payload value='#[readUrl("classpath://test-data/account-event.json", "application/json")]'/>
121
+ <flow-ref name="account-process-subflow"/>
122
+ </munit:execution>
123
+
124
+ <munit:validation>
125
+ <munit-tools:assert-that expression="#[payload.status]"
126
+ is="#[MunitTools::equalTo('success')]"/>
127
+ </munit:validation>
128
+ </munit:test>
129
+ ```
130
+
131
+ ### Pattern 4: Test Organization
132
+
133
+ ```
134
+ src/test/
135
+ ├── munit/
136
+ │ ├── salesforce-upsert-test.xml # Operation-specific tests
137
+ │ ├── salesforce-create-test.xml
138
+ │ ├── salesforce-query-test.xml
139
+ │ ├── salesforce-delete-test.xml
140
+ │ ├── salesforce-process-subflow-test.xml # Routing tests
141
+ │ ├── error-handling-test.xml # Error scenario tests
142
+ │ └── common-test-resources.xml # Shared mocks
143
+ └── resources/
144
+ ├── log4j2-test.xml # Console-only, noise-suppressed
145
+ └── test-data/ # Test payloads
146
+ ├── account-event.json
147
+ └── order-request.json
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Key Principles
153
+
154
+ 1. **Mock all external dependencies** — never call real systems in unit tests
155
+ 2. **Test all error handler branches** — verify each HTTP status code / error type
156
+ 3. **Use descriptive test names** — names should describe the scenario being tested
157
+ 4. **Isolate tests** — each test should be independent (no shared state)
158
+ 5. **Separate test log config** — use `log4j2-test.xml` with console appender and suppressed noise
159
+
160
+ ---
161
+
162
+ ## Running Tests
163
+
164
+ ```bash
165
+ # Full test suite
166
+ mvn clean test -Dmule.env=dev -Dsecure.key=test
167
+
168
+ # Specific test suite
169
+ mvn test -Dmule.env=dev -Dsecure.key=test -Dtest=salesforce-upsert-test
170
+
171
+ # Package (skip tests)
172
+ mvn clean package -DskipTests
173
+ ```
174
+
175
+ > **Note:** MUnit requires MuleSoft Enterprise Edition license. If the `licm` check fails in your dev environment, verify with `mvn process-classes` (schema validation passes without EE license).
176
+
177
+ ---
178
+
179
+ ## Checklist
180
+
181
+ - [ ] 80%+ flow coverage with MUnit
182
+ - [ ] All error handler branches tested
183
+ - [ ] External dependencies mocked (HTTP, connectors, databases)
184
+ - [ ] Test names clearly describe the scenario
185
+ - [ ] `log4j2-test.xml` configured for test environment
186
+ - [ ] Test data externalized to `test-data/` directory
187
+
188
+ ---
189
+
190
+ **See also:** [Error Handling](error-handling.md) · [CI/CD Integration](ci-cd.md)