busbar_sf_rest/client/
layout.rs1use tracing::instrument;
2
3use busbar_sf_client::security::{soql, url as url_security};
4
5use crate::error::{Error, ErrorKind, Result};
6
7impl super::SalesforceRestClient {
8 #[instrument(skip(self))]
22 pub async fn describe_layouts(
23 &self,
24 sobject: &str,
25 ) -> Result<crate::layout::DescribeLayoutsResult> {
26 if !soql::is_safe_sobject_name(sobject) {
27 return Err(Error::new(ErrorKind::Salesforce {
28 error_code: "INVALID_SOBJECT".to_string(),
29 message: "Invalid SObject name".to_string(),
30 }));
31 }
32 let path = format!("sobjects/{}/describe/layouts", sobject);
33 self.client.rest_get(&path).await.map_err(Into::into)
34 }
35
36 #[instrument(skip(self))]
49 pub async fn describe_named_layout(
50 &self,
51 sobject: &str,
52 layout_name: &str,
53 ) -> Result<crate::layout::NamedLayoutResult> {
54 if !soql::is_safe_sobject_name(sobject) {
55 return Err(Error::new(ErrorKind::Salesforce {
56 error_code: "INVALID_SOBJECT".to_string(),
57 message: "Invalid SObject name".to_string(),
58 }));
59 }
60 let encoded_name = url_security::encode_param(layout_name);
62 let path = format!(
63 "sobjects/{}/describe/namedLayouts/{}",
64 sobject, encoded_name
65 );
66 self.client.rest_get(&path).await.map_err(Into::into)
67 }
68
69 #[instrument(skip(self))]
83 pub async fn describe_approval_layouts(
84 &self,
85 sobject: &str,
86 ) -> Result<crate::layout::ApprovalLayoutsResult> {
87 if !soql::is_safe_sobject_name(sobject) {
88 return Err(Error::new(ErrorKind::Salesforce {
89 error_code: "INVALID_SOBJECT".to_string(),
90 message: "Invalid SObject name".to_string(),
91 }));
92 }
93 let path = format!("sobjects/{}/describe/approvalLayouts", sobject);
94 self.client.rest_get(&path).await.map_err(Into::into)
95 }
96
97 #[instrument(skip(self))]
111 pub async fn describe_compact_layouts(
112 &self,
113 sobject: &str,
114 ) -> Result<crate::layout::CompactLayoutsResult> {
115 if !soql::is_safe_sobject_name(sobject) {
116 return Err(Error::new(ErrorKind::Salesforce {
117 error_code: "INVALID_SOBJECT".to_string(),
118 message: "Invalid SObject name".to_string(),
119 }));
120 }
121 let path = format!("sobjects/{}/describe/compactLayouts", sobject);
122 self.client.rest_get(&path).await.map_err(Into::into)
123 }
124
125 #[instrument(skip(self))]
139 pub async fn describe_global_publisher_layouts(
140 &self,
141 ) -> Result<crate::layout::GlobalPublisherLayoutsResult> {
142 let path = "sobjects/Global/describe/layouts";
143 self.client.rest_get(path).await.map_err(Into::into)
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::super::SalesforceRestClient;
150
151 #[tokio::test]
152 async fn test_describe_layouts_wiremock() {
153 use wiremock::matchers::{method, path_regex};
154 use wiremock::{Mock, MockServer, ResponseTemplate};
155
156 let mock_server = MockServer::start().await;
157
158 let body = serde_json::json!({
159 "layouts": [{"id": "00h000000000001", "name": "Account Layout"}],
160 "recordTypeMappings": []
161 });
162
163 Mock::given(method("GET"))
164 .and(path_regex(".*/sobjects/Account/describe/layouts$"))
165 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
166 .mount(&mock_server)
167 .await;
168
169 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
170 let result = client
171 .describe_layouts("Account")
172 .await
173 .expect("describe_layouts should succeed");
174
175 assert!(result["layouts"].is_array());
176 assert_eq!(result["layouts"][0]["name"], "Account Layout");
177 }
178
179 #[tokio::test]
180 async fn test_describe_layouts_invalid_sobject() {
181 let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
182 let result = client.describe_layouts("Bad'; DROP--").await;
183 assert!(result.is_err());
184 assert!(result.unwrap_err().to_string().contains("INVALID_SOBJECT"));
185 }
186
187 #[tokio::test]
188 async fn test_describe_named_layout_wiremock() {
189 use wiremock::matchers::{method, path_regex};
190 use wiremock::{Mock, MockServer, ResponseTemplate};
191
192 let mock_server = MockServer::start().await;
193
194 let body = serde_json::json!({
195 "layouts": [{"detailLayoutSections": [], "editLayoutSections": []}]
196 });
197
198 Mock::given(method("GET"))
199 .and(path_regex(
200 ".*/sobjects/Account/describe/namedLayouts/MyLayout",
201 ))
202 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
203 .mount(&mock_server)
204 .await;
205
206 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
207 let result = client
208 .describe_named_layout("Account", "MyLayout")
209 .await
210 .expect("describe_named_layout should succeed");
211
212 assert!(result["layouts"].is_array());
213 }
214
215 #[tokio::test]
216 async fn test_describe_approval_layouts_wiremock() {
217 use wiremock::matchers::{method, path_regex};
218 use wiremock::{Mock, MockServer, ResponseTemplate};
219
220 let mock_server = MockServer::start().await;
221
222 let body = serde_json::json!({
223 "approvalLayouts": []
224 });
225
226 Mock::given(method("GET"))
227 .and(path_regex(".*/sobjects/Account/describe/approvalLayouts$"))
228 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
229 .mount(&mock_server)
230 .await;
231
232 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
233 let result = client
234 .describe_approval_layouts("Account")
235 .await
236 .expect("describe_approval_layouts should succeed");
237
238 assert!(result["approvalLayouts"].is_array());
239 }
240
241 #[tokio::test]
242 async fn test_describe_compact_layouts_wiremock() {
243 use wiremock::matchers::{method, path_regex};
244 use wiremock::{Mock, MockServer, ResponseTemplate};
245
246 let mock_server = MockServer::start().await;
247
248 let body = serde_json::json!({
249 "compactLayouts": [{"id": "0AH000000000001", "name": "System Default"}],
250 "defaultCompactLayoutId": "0AH000000000001"
251 });
252
253 Mock::given(method("GET"))
254 .and(path_regex(".*/sobjects/Account/describe/compactLayouts$"))
255 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
256 .mount(&mock_server)
257 .await;
258
259 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
260 let result = client
261 .describe_compact_layouts("Account")
262 .await
263 .expect("describe_compact_layouts should succeed");
264
265 assert!(result["compactLayouts"].is_array());
266 assert_eq!(result["compactLayouts"][0]["name"], "System Default");
267 }
268
269 #[tokio::test]
270 async fn test_describe_global_publisher_layouts_wiremock() {
271 use wiremock::matchers::{method, path_regex};
272 use wiremock::{Mock, MockServer, ResponseTemplate};
273
274 let mock_server = MockServer::start().await;
275
276 let body = serde_json::json!({
277 "layouts": [{"id": "00h000000000002", "name": "Global Layout"}]
278 });
279
280 Mock::given(method("GET"))
281 .and(path_regex(".*/sobjects/Global/describe/layouts$"))
282 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
283 .mount(&mock_server)
284 .await;
285
286 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
287 let result = client
288 .describe_global_publisher_layouts()
289 .await
290 .expect("describe_global_publisher_layouts should succeed");
291
292 assert!(result["layouts"].is_array());
293 assert_eq!(result["layouts"][0]["name"], "Global Layout");
294 }
295}