busbar_sf_rest/client/
process.rs1use serde::Deserialize;
2use tracing::instrument;
3
4use busbar_sf_client::security::soql;
5
6use crate::error::{Error, ErrorKind, Result};
7use crate::process::{
8 ApprovalRequest, ApprovalResult, PendingApprovalCollection, ProcessRule, ProcessRuleCollection,
9 ProcessRuleRequest, ProcessRuleResult,
10};
11
12#[derive(Deserialize)]
15struct PerSObjectProcessRules {
16 #[serde(default)]
17 rules: Vec<ProcessRule>,
18}
19
20impl super::SalesforceRestClient {
21 #[instrument(skip(self))]
23 pub async fn list_process_rules(&self) -> Result<ProcessRuleCollection> {
24 self.client
25 .rest_get("process/rules")
26 .await
27 .map_err(Into::into)
28 }
29
30 #[instrument(skip(self))]
34 pub async fn list_process_rules_for_sobject(
35 &self,
36 sobject: &str,
37 ) -> Result<Vec<crate::process::ProcessRule>> {
38 if !soql::is_safe_sobject_name(sobject) {
39 return Err(Error::new(ErrorKind::Salesforce {
40 error_code: "INVALID_SOBJECT".to_string(),
41 message: "Invalid SObject name".to_string(),
42 }));
43 }
44 let path = format!("process/rules/{}", sobject);
45 let response: PerSObjectProcessRules = self.client.rest_get(&path).await?;
48 Ok(response.rules)
49 }
50
51 #[instrument(skip(self, request))]
53 pub async fn trigger_process_rules(
54 &self,
55 request: &ProcessRuleRequest,
56 ) -> Result<ProcessRuleResult> {
57 self.client
58 .rest_post("process/rules", request)
59 .await
60 .map_err(Into::into)
61 }
62
63 #[instrument(skip(self))]
65 pub async fn list_pending_approvals(&self) -> Result<PendingApprovalCollection> {
66 self.client
67 .rest_get("process/approvals")
68 .await
69 .map_err(Into::into)
70 }
71
72 #[instrument(skip(self, request))]
76 pub async fn submit_approval(&self, request: &ApprovalRequest) -> Result<ApprovalResult> {
77 let wrapper = serde_json::json!({ "requests": [request] });
78 let results: Vec<ApprovalResult> =
79 self.client.rest_post("process/approvals", &wrapper).await?;
80 results.into_iter().next().ok_or_else(|| {
81 Error::new(ErrorKind::Salesforce {
82 error_code: "EMPTY_RESPONSE".to_string(),
83 message: "No approval result returned".to_string(),
84 })
85 })
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::super::SalesforceRestClient;
92
93 #[tokio::test]
94 async fn test_list_process_rules_for_sobject_invalid() {
95 let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
96 let result = client.list_process_rules_for_sobject("Bad'; DROP--").await;
97 assert!(result.is_err());
98 assert!(result.unwrap_err().to_string().contains("INVALID_SOBJECT"));
99 }
100
101 #[tokio::test]
102 async fn test_list_process_rules_wiremock() {
103 use wiremock::matchers::{method, path_regex};
104 use wiremock::{Mock, MockServer, ResponseTemplate};
105
106 let mock_server = MockServer::start().await;
107
108 let body = serde_json::json!({
109 "rules": {
110 "Account": [{
111 "id": "01Qxx0000000001",
112 "name": "My Rule",
113 "sobjectType": "Account",
114 "url": "/rules/Account/01Qxx0000000001"
115 }]
116 }
117 });
118
119 Mock::given(method("GET"))
120 .and(path_regex(".*/process/rules$"))
121 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
122 .mount(&mock_server)
123 .await;
124
125 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
126 let result = client
127 .list_process_rules()
128 .await
129 .expect("list_process_rules should succeed");
130 assert_eq!(result.rules.get("Account").unwrap().len(), 1);
131 }
132
133 #[tokio::test]
134 async fn test_list_process_rules_for_sobject_wiremock() {
135 use wiremock::matchers::{method, path_regex};
136 use wiremock::{Mock, MockServer, ResponseTemplate};
137
138 let mock_server = MockServer::start().await;
139
140 let body = serde_json::json!({
142 "rules": [{
143 "id": "01Qxx0000000001",
144 "name": "My Rule",
145 "sobjectType": "Account"
146 }]
147 });
148
149 Mock::given(method("GET"))
150 .and(path_regex(".*/process/rules/Account$"))
151 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
152 .mount(&mock_server)
153 .await;
154
155 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
156 let rules = client
157 .list_process_rules_for_sobject("Account")
158 .await
159 .expect("list_process_rules_for_sobject should succeed");
160 assert_eq!(rules.len(), 1);
161 assert_eq!(rules[0].name, "My Rule");
162 }
163
164 #[tokio::test]
165 async fn test_trigger_process_rules_wiremock() {
166 use wiremock::matchers::{method, path_regex};
167 use wiremock::{Mock, MockServer, ResponseTemplate};
168
169 let mock_server = MockServer::start().await;
170
171 let body = serde_json::json!({"errors": [], "success": true});
172
173 Mock::given(method("POST"))
174 .and(path_regex(".*/process/rules$"))
175 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
176 .mount(&mock_server)
177 .await;
178
179 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
180 let request = crate::process::ProcessRuleRequest {
181 context_ids: vec!["001xx000003DgAAAS".to_string()],
182 };
183 let result = client
184 .trigger_process_rules(&request)
185 .await
186 .expect("trigger_process_rules should succeed");
187 assert!(result.success);
188 }
189
190 #[tokio::test]
191 async fn test_list_pending_approvals_wiremock() {
192 use wiremock::matchers::{method, path_regex};
193 use wiremock::{Mock, MockServer, ResponseTemplate};
194
195 let mock_server = MockServer::start().await;
196
197 let body = serde_json::json!({
198 "approvals": {
199 "Account": [{
200 "id": "04axx0000000001",
201 "name": "Account_Approval",
202 "description": null,
203 "object": "Account",
204 "sortOrder": 1
205 }]
206 }
207 });
208
209 Mock::given(method("GET"))
210 .and(path_regex(".*/process/approvals$"))
211 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
212 .mount(&mock_server)
213 .await;
214
215 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
216 let result = client
217 .list_pending_approvals()
218 .await
219 .expect("list_pending_approvals should succeed");
220 assert_eq!(result.approvals.get("Account").unwrap().len(), 1);
221 }
222
223 #[tokio::test]
224 async fn test_submit_approval_wiremock() {
225 use wiremock::matchers::{method, path_regex};
226 use wiremock::{Mock, MockServer, ResponseTemplate};
227
228 let mock_server = MockServer::start().await;
229
230 let body = serde_json::json!([{
231 "actorIds": ["005xx000001Svf0AAC"],
232 "entityId": "001xx000003DgAAAS",
233 "errors": [],
234 "instanceId": "04gxx0000000001",
235 "instanceStatus": "Pending",
236 "newWorkitemIds": ["04ixx0000000002"],
237 "success": true
238 }]);
239
240 Mock::given(method("POST"))
241 .and(path_regex(".*/process/approvals$"))
242 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
243 .mount(&mock_server)
244 .await;
245
246 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
247 let request = crate::process::ApprovalRequest {
248 action_type: crate::process::ApprovalActionType::Submit,
249 context_id: "001xx000003DgAAAS".to_string(),
250 context_actor_id: None,
251 comments: Some("Please approve".to_string()),
252 next_approver_ids: None,
253 process_definition_name_or_id: None,
254 skip_entry_criteria: None,
255 };
256 let result = client
257 .submit_approval(&request)
258 .await
259 .expect("submit_approval should succeed");
260 assert!(result.success);
261 assert_eq!(result.instance_status, "Pending");
262 }
263}