busbar_sf_rest/client/
standalone.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))]
12 pub async fn tabs(&self) -> Result<Vec<serde_json::Value>> {
13 self.client.rest_get("tabs").await.map_err(Into::into)
14 }
15
16 #[instrument(skip(self))]
20 pub async fn theme(&self) -> Result<serde_json::Value> {
21 self.client.rest_get("theme").await.map_err(Into::into)
22 }
23
24 #[instrument(skip(self))]
29 pub async fn app_menu(&self, app_menu_type: &str) -> Result<serde_json::Value> {
30 let valid_types = ["AppSwitcher", "Salesforce1", "NetworkTabs"];
31 if !valid_types.contains(&app_menu_type) {
32 return Err(Error::new(ErrorKind::Salesforce {
33 error_code: "INVALID_PARAMETER".to_string(),
34 message: format!(
35 "Invalid app menu type '{}'. Must be one of: AppSwitcher, Salesforce1, NetworkTabs",
36 app_menu_type
37 ),
38 }));
39 }
40 let path = format!("appMenu/{}", app_menu_type);
41 self.client.rest_get(&path).await.map_err(Into::into)
42 }
43
44 #[instrument(skip(self))]
48 pub async fn recent_items(&self) -> Result<Vec<serde_json::Value>> {
49 self.client.rest_get("recent").await.map_err(Into::into)
50 }
51
52 #[instrument(skip(self))]
56 pub async fn relevant_items(&self) -> Result<serde_json::Value> {
57 self.client
58 .rest_get("sobjects/relevantItems")
59 .await
60 .map_err(Into::into)
61 }
62
63 #[instrument(skip(self))]
68 pub async fn compact_layouts(&self, sobject_list: &str) -> Result<serde_json::Value> {
69 for sobject in sobject_list.split(',') {
71 let trimmed = sobject.trim();
72 if !soql::is_safe_sobject_name(trimmed) {
73 return Err(Error::new(ErrorKind::Salesforce {
74 error_code: "INVALID_SOBJECT".to_string(),
75 message: format!("Invalid SObject name: {}", trimmed),
76 }));
77 }
78 }
79 let encoded = url_security::encode_param(sobject_list);
80 let path = format!("compactLayouts?q={}", encoded);
81 self.client.rest_get(&path).await.map_err(Into::into)
82 }
83
84 #[instrument(skip(self))]
89 pub async fn platform_event_schema(&self, event_name: &str) -> Result<serde_json::Value> {
90 if !soql::is_safe_sobject_name(event_name) {
91 return Err(Error::new(ErrorKind::Salesforce {
92 error_code: "INVALID_EVENT_NAME".to_string(),
93 message: "Invalid platform event name".to_string(),
94 }));
95 }
96 let path = format!("event/eventSchema/{}", event_name);
97 self.client.rest_get(&path).await.map_err(Into::into)
98 }
99
100 #[instrument(skip(self))]
104 pub async fn lightning_toggle_metrics(&self) -> Result<serde_json::Value> {
105 self.client
106 .rest_get("lightning/toggleMetrics")
107 .await
108 .map_err(Into::into)
109 }
110
111 #[instrument(skip(self))]
115 pub async fn lightning_usage(&self) -> Result<serde_json::Value> {
116 self.client
117 .rest_get("lightning/usage")
118 .await
119 .map_err(Into::into)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::super::SalesforceRestClient;
126
127 #[tokio::test]
128 async fn test_tabs_wiremock() {
129 use wiremock::matchers::{method, path_regex};
130 use wiremock::{Mock, MockServer, ResponseTemplate};
131
132 let mock_server = MockServer::start().await;
133
134 let body = serde_json::json!([
135 {"label": "Accounts", "name": "standard-Account", "url": "/001/o"}
136 ]);
137
138 Mock::given(method("GET"))
139 .and(path_regex(".*/tabs$"))
140 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
141 .mount(&mock_server)
142 .await;
143
144 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
145 let result = client.tabs().await.expect("tabs should succeed");
146 assert_eq!(result.len(), 1);
147 assert_eq!(result[0]["name"], "standard-Account");
148 }
149
150 #[tokio::test]
151 async fn test_theme_wiremock() {
152 use wiremock::matchers::{method, path_regex};
153 use wiremock::{Mock, MockServer, ResponseTemplate};
154
155 let mock_server = MockServer::start().await;
156
157 let body = serde_json::json!({
158 "themeItems": [{"name": "Account", "colors": []}]
159 });
160
161 Mock::given(method("GET"))
162 .and(path_regex(".*/theme$"))
163 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
164 .mount(&mock_server)
165 .await;
166
167 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
168 let result = client.theme().await.expect("theme should succeed");
169 assert!(result["themeItems"].is_array());
170 }
171
172 #[tokio::test]
173 async fn test_app_menu_valid_type() {
174 use wiremock::matchers::{method, path_regex};
175 use wiremock::{Mock, MockServer, ResponseTemplate};
176
177 let mock_server = MockServer::start().await;
178
179 let body = serde_json::json!({
180 "appMenuItems": []
181 });
182
183 Mock::given(method("GET"))
184 .and(path_regex(".*/appMenu/AppSwitcher$"))
185 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
186 .mount(&mock_server)
187 .await;
188
189 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
190 let result = client
191 .app_menu("AppSwitcher")
192 .await
193 .expect("app_menu should succeed");
194 assert!(result["appMenuItems"].is_array());
195 }
196
197 #[tokio::test]
198 async fn test_app_menu_invalid_type() {
199 let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
200 let result = client.app_menu("InvalidType").await;
201 assert!(result.is_err());
202 assert!(result
203 .unwrap_err()
204 .to_string()
205 .contains("INVALID_PARAMETER"));
206 }
207
208 #[tokio::test]
209 async fn test_recent_items_wiremock() {
210 use wiremock::matchers::{method, path_regex};
211 use wiremock::{Mock, MockServer, ResponseTemplate};
212
213 let mock_server = MockServer::start().await;
214
215 let body = serde_json::json!([
216 {"Id": "001xx000003Dgb2AAC", "Name": "Acme Corp"}
217 ]);
218
219 Mock::given(method("GET"))
220 .and(path_regex(".*/recent$"))
221 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
222 .mount(&mock_server)
223 .await;
224
225 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
226 let result = client
227 .recent_items()
228 .await
229 .expect("recent_items should succeed");
230 assert_eq!(result.len(), 1);
231 }
232
233 #[tokio::test]
234 async fn test_relevant_items_wiremock() {
235 use wiremock::matchers::{method, path_regex};
236 use wiremock::{Mock, MockServer, ResponseTemplate};
237
238 let mock_server = MockServer::start().await;
239
240 let body = serde_json::json!({
241 "relevantItems": []
242 });
243
244 Mock::given(method("GET"))
245 .and(path_regex(".*/sobjects/relevantItems$"))
246 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
247 .mount(&mock_server)
248 .await;
249
250 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
251 let result = client
252 .relevant_items()
253 .await
254 .expect("relevant_items should succeed");
255 assert!(result["relevantItems"].is_array());
256 }
257
258 #[tokio::test]
259 async fn test_compact_layouts_wiremock() {
260 use wiremock::matchers::{method, path_regex};
261 use wiremock::{Mock, MockServer, ResponseTemplate};
262
263 let mock_server = MockServer::start().await;
264
265 let body = serde_json::json!({
266 "Account": {"compactLayouts": []}
267 });
268
269 Mock::given(method("GET"))
270 .and(path_regex(".*/compactLayouts.*"))
271 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
272 .mount(&mock_server)
273 .await;
274
275 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
276 let result = client
277 .compact_layouts("Account")
278 .await
279 .expect("compact_layouts should succeed");
280 assert!(result["Account"].is_object());
281 }
282
283 #[tokio::test]
284 async fn test_compact_layouts_invalid_sobject() {
285 let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
286 let result = client.compact_layouts("Bad'; DROP--").await;
287 assert!(result.is_err());
288 assert!(result.unwrap_err().to_string().contains("INVALID_SOBJECT"));
289 }
290
291 #[tokio::test]
292 async fn test_platform_event_schema_wiremock() {
293 use wiremock::matchers::{method, path_regex};
294 use wiremock::{Mock, MockServer, ResponseTemplate};
295
296 let mock_server = MockServer::start().await;
297
298 let body = serde_json::json!({
299 "name": "MyEvent__e",
300 "fields": []
301 });
302
303 Mock::given(method("GET"))
304 .and(path_regex(".*/event/eventSchema/MyEvent__e$"))
305 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
306 .mount(&mock_server)
307 .await;
308
309 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
310 let result = client
311 .platform_event_schema("MyEvent__e")
312 .await
313 .expect("platform_event_schema should succeed");
314 assert_eq!(result["name"], "MyEvent__e");
315 }
316
317 #[tokio::test]
318 async fn test_platform_event_schema_invalid_name() {
319 let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
320 let result = client.platform_event_schema("Bad'; DROP--").await;
321 assert!(result.is_err());
322 assert!(result
323 .unwrap_err()
324 .to_string()
325 .contains("INVALID_EVENT_NAME"));
326 }
327
328 #[tokio::test]
329 async fn test_lightning_toggle_metrics_wiremock() {
330 use wiremock::matchers::{method, path_regex};
331 use wiremock::{Mock, MockServer, ResponseTemplate};
332
333 let mock_server = MockServer::start().await;
334
335 let body = serde_json::json!({
336 "metricsData": []
337 });
338
339 Mock::given(method("GET"))
340 .and(path_regex(".*/lightning/toggleMetrics$"))
341 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
342 .mount(&mock_server)
343 .await;
344
345 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
346 let result = client
347 .lightning_toggle_metrics()
348 .await
349 .expect("lightning_toggle_metrics should succeed");
350 assert!(result["metricsData"].is_array());
351 }
352
353 #[tokio::test]
354 async fn test_lightning_usage_wiremock() {
355 use wiremock::matchers::{method, path_regex};
356 use wiremock::{Mock, MockServer, ResponseTemplate};
357
358 let mock_server = MockServer::start().await;
359
360 let body = serde_json::json!({
361 "usageData": []
362 });
363
364 Mock::given(method("GET"))
365 .and(path_regex(".*/lightning/usage$"))
366 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
367 .mount(&mock_server)
368 .await;
369
370 let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
371 let result = client
372 .lightning_usage()
373 .await
374 .expect("lightning_usage should succeed");
375 assert!(result["usageData"].is_array());
376 }
377}