@sfdxy/mule-lint 1.21.0 → 1.22.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/dist/package.json +1 -1
- package/dist/src/mcp/prompts/index.d.ts +1 -1
- package/dist/src/mcp/prompts/index.d.ts.map +1 -1
- package/dist/src/mcp/prompts/index.js +62 -1
- package/dist/src/mcp/prompts/index.js.map +1 -1
- package/dist/src/mcp/resources/index.js +114 -16
- package/dist/src/mcp/resources/index.js.map +1 -1
- package/dist/src/mcp/tools/getRuleDetails.d.ts.map +1 -1
- package/dist/src/mcp/tools/getRuleDetails.js +30 -1
- package/dist/src/mcp/tools/getRuleDetails.js.map +1 -1
- package/docs/README.md +87 -27
- package/docs/best-practices/ci-cd.md +135 -0
- package/docs/best-practices/connector-patterns.md +253 -0
- package/docs/best-practices/dataweave-patterns.md +370 -0
- package/docs/best-practices/deployment-2026.md +171 -0
- package/docs/best-practices/error-handling.md +277 -0
- package/docs/best-practices/event-driven-patterns.md +424 -0
- package/docs/best-practices/logging.md +163 -0
- package/docs/best-practices/mulesoft-best-practices.md +72 -865
- package/docs/best-practices/performance.md +273 -0
- package/docs/best-practices/security.md +181 -0
- package/docs/best-practices/testing.md +190 -0
- package/docs/best-practices/variable-contracts.md +191 -0
- package/package.json +1 -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)
|