Skip to main content

busbar_sf_rest/client/
consent.rs

1use tracing::instrument;
2
3use busbar_sf_client::security::soql;
4
5use crate::consent::{ConsentResponse, ConsentWriteRequest};
6use crate::error::{Error, ErrorKind, Result};
7
8impl super::SalesforceRestClient {
9    /// Read consent status for an action and a set of IDs.
10    #[instrument(skip(self))]
11    pub async fn read_consent(&self, action: &str, ids: &[&str]) -> Result<ConsentResponse> {
12        if !soql::is_safe_field_name(action) {
13            return Err(Error::new(ErrorKind::Salesforce {
14                error_code: "INVALID_ACTION".to_string(),
15                message: "Invalid consent action name".to_string(),
16            }));
17        }
18        let ids_param = ids.join(",");
19        let path = format!("consent/action/{}?ids={}", action, ids_param);
20        self.client.rest_get(&path).await.map_err(Into::into)
21    }
22
23    /// Write consent for an action (uses PATCH, not POST).
24    #[instrument(skip(self, request))]
25    pub async fn write_consent(&self, action: &str, request: &ConsentWriteRequest) -> Result<()> {
26        if !soql::is_safe_field_name(action) {
27            return Err(Error::new(ErrorKind::Salesforce {
28                error_code: "INVALID_ACTION".to_string(),
29                message: "Invalid consent action name".to_string(),
30            }));
31        }
32        let path = format!("consent/action/{}", action);
33        self.client
34            .rest_patch(&path, request)
35            .await
36            .map_err(Into::into)
37    }
38
39    /// Read consent for multiple actions and IDs.
40    #[instrument(skip(self))]
41    pub async fn read_multi_consent(
42        &self,
43        actions: &[&str],
44        ids: &[&str],
45    ) -> Result<serde_json::Value> {
46        let actions_param = actions.join(",");
47        let ids_param = ids.join(",");
48        let path = format!(
49            "consent/multiaction?actions={}&ids={}",
50            actions_param, ids_param
51        );
52        self.client.rest_get(&path).await.map_err(Into::into)
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::super::SalesforceRestClient;
59
60    #[tokio::test]
61    async fn test_read_consent_invalid_action() {
62        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
63        let result = client.read_consent("Bad'; DROP--", &["001xx"]).await;
64        assert!(result.is_err());
65        assert!(result.unwrap_err().to_string().contains("INVALID_ACTION"));
66    }
67
68    #[tokio::test]
69    async fn test_write_consent_invalid_action() {
70        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
71        let request = crate::consent::ConsentWriteRequest { records: vec![] };
72        let result = client.write_consent("Bad'; DROP--", &request).await;
73        assert!(result.is_err());
74        assert!(result.unwrap_err().to_string().contains("INVALID_ACTION"));
75    }
76
77    #[tokio::test]
78    async fn test_read_consent_wiremock() {
79        use wiremock::matchers::{method, path_regex};
80        use wiremock::{Mock, MockServer, ResponseTemplate};
81
82        let mock_server = MockServer::start().await;
83
84        let body = serde_json::json!({
85            "results": [{
86                "result": "OptIn",
87                "status": "Active",
88                "objectConsulted": "ContactPointEmail"
89            }]
90        });
91
92        Mock::given(method("GET"))
93            .and(path_regex(".*/consent/action/email.*"))
94            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
95            .mount(&mock_server)
96            .await;
97
98        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
99        let result = client
100            .read_consent("email", &["001xx000003DgAAAS"])
101            .await
102            .expect("read_consent should succeed");
103        assert_eq!(result.results.len(), 1);
104        assert_eq!(result.results[0].result, "OptIn");
105    }
106
107    #[tokio::test]
108    async fn test_write_consent_wiremock() {
109        use wiremock::matchers::{method, path_regex};
110        use wiremock::{Mock, MockServer, ResponseTemplate};
111
112        let mock_server = MockServer::start().await;
113
114        Mock::given(method("PATCH"))
115            .and(path_regex(".*/consent/action/email$"))
116            .respond_with(ResponseTemplate::new(204))
117            .mount(&mock_server)
118            .await;
119
120        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
121        let request = crate::consent::ConsentWriteRequest {
122            records: vec![crate::consent::ConsentWriteRecord {
123                id: "001xx000003DgAAAS".to_string(),
124                result: "OptIn".to_string(),
125            }],
126        };
127        client
128            .write_consent("email", &request)
129            .await
130            .expect("write_consent should succeed");
131    }
132
133    #[tokio::test]
134    async fn test_read_multi_consent_wiremock() {
135        use wiremock::matchers::{method, path_regex};
136        use wiremock::{Mock, MockServer, ResponseTemplate};
137
138        let mock_server = MockServer::start().await;
139
140        let body = serde_json::json!({
141            "results": [
142                {"action": "email", "status": "OptIn"},
143                {"action": "sms", "status": "OptOut"}
144            ]
145        });
146
147        Mock::given(method("GET"))
148            .and(path_regex(".*/consent/multiaction.*"))
149            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
150            .mount(&mock_server)
151            .await;
152
153        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
154        let result = client
155            .read_multi_consent(&["email", "sms"], &["001xx000003DgAAAS"])
156            .await
157            .expect("read_multi_consent should succeed");
158        assert!(result["results"].is_array());
159    }
160}