Skip to main content

busbar_sf_rest/client/
standalone.rs

1use 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    /// Get all tabs available to the current user.
9    ///
10    /// Returns information about all tabs, including custom tabs.
11    #[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    /// Get the current user's theme information.
17    ///
18    /// Returns theme colors, icons, and other UI customization data.
19    #[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    /// Get the app menu for a specific menu type.
25    ///
26    /// # Arguments
27    /// * `app_menu_type` - One of: "AppSwitcher", "Salesforce1", "NetworkTabs"
28    #[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    /// Get recently viewed items for the current user.
45    ///
46    /// Returns a list of recently accessed records.
47    #[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    /// Get relevant items for the current user.
53    ///
54    /// Returns items that Salesforce considers relevant based on the user's activity.
55    #[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    /// Get compact layouts for multiple SObject types.
64    ///
65    /// # Arguments
66    /// * `sobject_list` - Comma-separated list of SObject API names (e.g., "Account,Contact")
67    #[instrument(skip(self))]
68    pub async fn compact_layouts(&self, sobject_list: &str) -> Result<serde_json::Value> {
69        // Validate each SObject name in the comma-separated list
70        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    /// Get the event schema for a platform event.
85    ///
86    /// # Arguments
87    /// * `event_name` - The platform event API name (e.g., "MyEvent__e")
88    #[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    /// Get Lightning Experience toggle metrics.
101    ///
102    /// Returns metrics about Lightning Experience vs Classic usage.
103    #[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    /// Get Lightning Experience usage data.
112    ///
113    /// Returns Lightning Experience usage statistics.
114    #[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}