Skip to main content

busbar_sf_rest/client/
sync.rs

1use tracing::instrument;
2
3use busbar_sf_client::security::soql;
4
5use crate::error::{Error, ErrorKind, Result};
6
7impl super::SalesforceRestClient {
8    /// Get deleted records for an SObject type within a date range.
9    ///
10    /// The start and end parameters should be ISO 8601 date-time strings
11    /// (e.g., "2024-01-01T00:00:00Z").
12    #[instrument(skip(self))]
13    pub async fn get_deleted(
14        &self,
15        sobject: &str,
16        start: &str,
17        end: &str,
18    ) -> Result<super::GetDeletedResult> {
19        if !soql::is_safe_sobject_name(sobject) {
20            return Err(Error::new(ErrorKind::Salesforce {
21                error_code: "INVALID_SOBJECT".to_string(),
22                message: "Invalid SObject name".to_string(),
23            }));
24        }
25        let path = format!(
26            "sobjects/{}/deleted/?start={}&end={}",
27            sobject,
28            urlencoding::encode(start),
29            urlencoding::encode(end)
30        );
31        self.client.rest_get(&path).await.map_err(Into::into)
32    }
33
34    /// Get updated record IDs for an SObject type within a date range.
35    ///
36    /// The start and end parameters should be ISO 8601 date-time strings
37    /// (e.g., "2024-01-01T00:00:00Z").
38    #[instrument(skip(self))]
39    pub async fn get_updated(
40        &self,
41        sobject: &str,
42        start: &str,
43        end: &str,
44    ) -> Result<super::GetUpdatedResult> {
45        if !soql::is_safe_sobject_name(sobject) {
46            return Err(Error::new(ErrorKind::Salesforce {
47                error_code: "INVALID_SOBJECT".to_string(),
48                message: "Invalid SObject name".to_string(),
49            }));
50        }
51        let path = format!(
52            "sobjects/{}/updated/?start={}&end={}",
53            sobject,
54            urlencoding::encode(start),
55            urlencoding::encode(end)
56        );
57        self.client.rest_get(&path).await.map_err(Into::into)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::super::SalesforceRestClient;
64
65    #[tokio::test]
66    async fn test_get_deleted_invalid_sobject() {
67        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
68        let result = client
69            .get_deleted(
70                "Bad'; DROP--",
71                "2024-01-01T00:00:00Z",
72                "2024-01-15T00:00:00Z",
73            )
74            .await;
75        assert!(result.is_err());
76        assert!(result.unwrap_err().to_string().contains("INVALID_SOBJECT"));
77    }
78
79    #[tokio::test]
80    async fn test_get_updated_invalid_sobject() {
81        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
82        let result = client
83            .get_updated(
84                "Bad'; DROP--",
85                "2024-01-01T00:00:00Z",
86                "2024-01-15T00:00:00Z",
87            )
88            .await;
89        assert!(result.is_err());
90        assert!(result.unwrap_err().to_string().contains("INVALID_SOBJECT"));
91    }
92
93    #[tokio::test]
94    async fn test_get_deleted_wiremock() {
95        use wiremock::matchers::{method, path_regex};
96        use wiremock::{Mock, MockServer, ResponseTemplate};
97
98        let mock_server = MockServer::start().await;
99
100        let body = serde_json::json!({
101            "deletedRecords": [
102                {"id": "001xx000003DgAAAS", "deletedDate": "2024-01-15T10:30:00.000Z"}
103            ],
104            "earliestDateAvailable": "2024-01-01T00:00:00.000Z",
105            "latestDateCovered": "2024-01-15T23:59:59.000Z"
106        });
107
108        Mock::given(method("GET"))
109            .and(path_regex(".*/sobjects/Account/deleted/.*"))
110            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
111            .mount(&mock_server)
112            .await;
113
114        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
115        let result = client
116            .get_deleted("Account", "2024-01-01T00:00:00Z", "2024-01-15T00:00:00Z")
117            .await
118            .expect("get_deleted should succeed");
119        assert_eq!(result.deleted_records.len(), 1);
120        assert_eq!(result.deleted_records[0].id, "001xx000003DgAAAS");
121    }
122
123    #[tokio::test]
124    async fn test_get_updated_wiremock() {
125        use wiremock::matchers::{method, path_regex};
126        use wiremock::{Mock, MockServer, ResponseTemplate};
127
128        let mock_server = MockServer::start().await;
129
130        let body = serde_json::json!({
131            "ids": ["001xx000003DgAAAS", "001xx000003DgBBAS"],
132            "latestDateCovered": "2024-01-15T23:59:59.000Z"
133        });
134
135        Mock::given(method("GET"))
136            .and(path_regex(".*/sobjects/Account/updated/.*"))
137            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
138            .mount(&mock_server)
139            .await;
140
141        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
142        let result = client
143            .get_updated("Account", "2024-01-01T00:00:00Z", "2024-01-15T00:00:00Z")
144            .await
145            .expect("get_updated should succeed");
146        assert_eq!(result.ids.len(), 2);
147    }
148}