Skip to main content

busbar_sf_rest/client/
list_views.rs

1use serde::de::DeserializeOwned;
2use tracing::instrument;
3
4use busbar_sf_client::security::{soql, url as url_security};
5
6use crate::error::{Error, ErrorKind, Result};
7use crate::list_views::{ListView, ListViewCollection, ListViewDescribe, ListViewResult};
8
9impl super::SalesforceRestClient {
10    /// List all list views for an SObject.
11    #[instrument(skip(self))]
12    pub async fn list_views(&self, sobject: &str) -> Result<ListViewCollection> {
13        if !soql::is_safe_sobject_name(sobject) {
14            return Err(Error::new(ErrorKind::Salesforce {
15                error_code: "INVALID_SOBJECT".to_string(),
16                message: "Invalid SObject name".to_string(),
17            }));
18        }
19        let path = format!("sobjects/{}/listviews", sobject);
20        self.client.rest_get(&path).await.map_err(Into::into)
21    }
22
23    /// Get a specific list view by ID.
24    #[instrument(skip(self))]
25    pub async fn get_list_view(&self, sobject: &str, list_view_id: &str) -> Result<ListView> {
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        if !url_security::is_valid_salesforce_id(list_view_id) {
33            return Err(Error::new(ErrorKind::Salesforce {
34                error_code: "INVALID_ID".to_string(),
35                message: "Invalid Salesforce ID format".to_string(),
36            }));
37        }
38        let path = format!("sobjects/{}/listviews/{}", sobject, list_view_id);
39        self.client.rest_get(&path).await.map_err(Into::into)
40    }
41
42    /// Describe a list view (get columns, filters, etc.).
43    #[instrument(skip(self))]
44    pub async fn describe_list_view(
45        &self,
46        sobject: &str,
47        list_view_id: &str,
48    ) -> Result<ListViewDescribe> {
49        if !soql::is_safe_sobject_name(sobject) {
50            return Err(Error::new(ErrorKind::Salesforce {
51                error_code: "INVALID_SOBJECT".to_string(),
52                message: "Invalid SObject name".to_string(),
53            }));
54        }
55        if !url_security::is_valid_salesforce_id(list_view_id) {
56            return Err(Error::new(ErrorKind::Salesforce {
57                error_code: "INVALID_ID".to_string(),
58                message: "Invalid Salesforce ID format".to_string(),
59            }));
60        }
61        let path = format!("sobjects/{}/listviews/{}/describe", sobject, list_view_id);
62        self.client.rest_get(&path).await.map_err(Into::into)
63    }
64
65    /// Execute a list view and return its results.
66    #[instrument(skip(self))]
67    pub async fn execute_list_view<T: DeserializeOwned>(
68        &self,
69        sobject: &str,
70        list_view_id: &str,
71    ) -> Result<ListViewResult<T>> {
72        if !soql::is_safe_sobject_name(sobject) {
73            return Err(Error::new(ErrorKind::Salesforce {
74                error_code: "INVALID_SOBJECT".to_string(),
75                message: "Invalid SObject name".to_string(),
76            }));
77        }
78        if !url_security::is_valid_salesforce_id(list_view_id) {
79            return Err(Error::new(ErrorKind::Salesforce {
80                error_code: "INVALID_ID".to_string(),
81                message: "Invalid Salesforce ID format".to_string(),
82            }));
83        }
84        let path = format!("sobjects/{}/listviews/{}/results", sobject, list_view_id);
85        self.client.rest_get(&path).await.map_err(Into::into)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::super::SalesforceRestClient;
92
93    #[tokio::test]
94    async fn test_list_views_invalid_sobject() {
95        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
96        let result = client.list_views("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_get_list_view_invalid_id() {
103        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
104        let result = client.get_list_view("Account", "bad-id").await;
105        assert!(result.is_err());
106        assert!(result.unwrap_err().to_string().contains("INVALID_ID"));
107    }
108
109    #[tokio::test]
110    async fn test_describe_list_view_invalid_sobject() {
111        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
112        let result = client
113            .describe_list_view("Bad'; DROP--", "00Bxx0000000001AAA")
114            .await;
115        assert!(result.is_err());
116        assert!(result.unwrap_err().to_string().contains("INVALID_SOBJECT"));
117    }
118
119    #[tokio::test]
120    async fn test_execute_list_view_invalid_id() {
121        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
122        let result = client
123            .execute_list_view::<serde_json::Value>("Account", "bad-id")
124            .await;
125        assert!(result.is_err());
126        assert!(result.unwrap_err().to_string().contains("INVALID_ID"));
127    }
128
129    #[tokio::test]
130    async fn test_list_views_wiremock() {
131        use wiremock::matchers::{method, path_regex};
132        use wiremock::{Mock, MockServer, ResponseTemplate};
133
134        let mock_server = MockServer::start().await;
135
136        let body = serde_json::json!({
137            "done": true,
138            "nextRecordsUrl": null,
139            "listviews": [{
140                "id": "00Bxx0000000001AAA",
141                "developerName": "AllAccounts",
142                "label": "All Accounts",
143                "describeUrl": "/describe",
144                "resultsUrl": "/results",
145                "sobjectType": "Account"
146            }]
147        });
148
149        Mock::given(method("GET"))
150            .and(path_regex(".*/sobjects/Account/listviews$"))
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 result = client
157            .list_views("Account")
158            .await
159            .expect("list_views should succeed");
160        assert!(result.done);
161        assert_eq!(result.listviews.len(), 1);
162        assert_eq!(result.listviews[0].developer_name, "AllAccounts");
163    }
164
165    #[tokio::test]
166    async fn test_get_list_view_wiremock() {
167        use wiremock::matchers::{method, path_regex};
168        use wiremock::{Mock, MockServer, ResponseTemplate};
169
170        let mock_server = MockServer::start().await;
171
172        let body = serde_json::json!({
173            "id": "00Bxx0000000001AAA",
174            "developerName": "AllAccounts",
175            "label": "All Accounts",
176            "describeUrl": "/describe",
177            "resultsUrl": "/results",
178            "sobjectType": "Account"
179        });
180
181        Mock::given(method("GET"))
182            .and(path_regex(
183                ".*/sobjects/Account/listviews/00Bxx0000000001AAA$",
184            ))
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            .get_list_view("Account", "00Bxx0000000001AAA")
192            .await
193            .expect("get_list_view should succeed");
194        assert_eq!(result.id, "00Bxx0000000001AAA");
195    }
196
197    #[tokio::test]
198    async fn test_describe_list_view_wiremock() {
199        use wiremock::matchers::{method, path_regex};
200        use wiremock::{Mock, MockServer, ResponseTemplate};
201
202        let mock_server = MockServer::start().await;
203
204        let body = serde_json::json!({
205            "id": "00Bxx0000000001AAA",
206            "developerName": "AllAccounts",
207            "label": "All Accounts",
208            "sobjectType": "Account",
209            "query": "SELECT Id, Name FROM Account",
210            "columns": [{
211                "fieldNameOrPath": "Name",
212                "label": "Account Name",
213                "sortable": true,
214                "type": "string"
215            }],
216            "orderBy": [],
217            "whereCondition": null
218        });
219
220        Mock::given(method("GET"))
221            .and(path_regex(
222                ".*/sobjects/Account/listviews/00Bxx0000000001AAA/describe$",
223            ))
224            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
225            .mount(&mock_server)
226            .await;
227
228        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
229        let result = client
230            .describe_list_view("Account", "00Bxx0000000001AAA")
231            .await
232            .expect("describe_list_view should succeed");
233        assert_eq!(result.columns.len(), 1);
234    }
235
236    #[tokio::test]
237    async fn test_execute_list_view_wiremock() {
238        use wiremock::matchers::{method, path_regex};
239        use wiremock::{Mock, MockServer, ResponseTemplate};
240
241        let mock_server = MockServer::start().await;
242
243        let body = serde_json::json!({
244            "done": true,
245            "id": "00Bxx0000000001AAA",
246            "label": "All Accounts",
247            "records": [{"Id": "001xx", "Name": "Acme"}],
248            "size": 1,
249            "developerName": "AllAccounts",
250            "nextRecordsUrl": null
251        });
252
253        Mock::given(method("GET"))
254            .and(path_regex(
255                ".*/sobjects/Account/listviews/00Bxx0000000001AAA/results$",
256            ))
257            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
258            .mount(&mock_server)
259            .await;
260
261        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
262        let result = client
263            .execute_list_view::<serde_json::Value>("Account", "00Bxx0000000001AAA")
264            .await
265            .expect("execute_list_view should succeed");
266        assert!(result.done);
267        assert_eq!(result.size, 1);
268    }
269}