Skip to main content

busbar_sf_rest/client/
knowledge.rs

1use tracing::instrument;
2
3use busbar_sf_client::security::soql;
4
5use crate::error::{Error, ErrorKind, Result};
6use crate::knowledge::{
7    DataCategoriesResponse, DataCategoryGroupsResponse, KnowledgeArticlesResponse,
8    KnowledgeSettings,
9};
10
11impl super::SalesforceRestClient {
12    /// Get knowledge management settings.
13    #[instrument(skip(self))]
14    pub async fn knowledge_settings(&self) -> Result<KnowledgeSettings> {
15        self.client
16            .rest_get("knowledgeManagement/settings")
17            .await
18            .map_err(Into::into)
19    }
20
21    /// List knowledge articles, optionally filtering by query string and channel.
22    #[instrument(skip(self))]
23    pub async fn knowledge_articles(
24        &self,
25        query: Option<&str>,
26        channel: Option<&str>,
27    ) -> Result<KnowledgeArticlesResponse> {
28        let mut path = "support/knowledgeArticles".to_string();
29        let mut params = Vec::new();
30        if let Some(q) = query {
31            params.push(format!("q={}", urlencoding::encode(q)));
32        }
33        if let Some(ch) = channel {
34            params.push(format!("channel={}", urlencoding::encode(ch)));
35        }
36        if !params.is_empty() {
37            path.push('?');
38            path.push_str(&params.join("&"));
39        }
40        self.client.rest_get(&path).await.map_err(Into::into)
41    }
42
43    /// Get data category groups, optionally filtered by SObject type.
44    #[instrument(skip(self))]
45    pub async fn data_category_groups(
46        &self,
47        sobject: Option<&str>,
48    ) -> Result<DataCategoryGroupsResponse> {
49        if let Some(s) = sobject {
50            if !soql::is_safe_sobject_name(s) {
51                return Err(Error::new(ErrorKind::Salesforce {
52                    error_code: "INVALID_SOBJECT".to_string(),
53                    message: "Invalid SObject name".to_string(),
54                }));
55            }
56        }
57        let path = match sobject {
58            Some(s) => format!("support/dataCategoryGroups?sObjectType={}", s),
59            None => "support/dataCategoryGroups".to_string(),
60        };
61        self.client.rest_get(&path).await.map_err(Into::into)
62    }
63
64    /// Get data categories within a group, optionally filtered by SObject type.
65    #[instrument(skip(self))]
66    pub async fn data_categories(
67        &self,
68        group: &str,
69        sobject: Option<&str>,
70    ) -> Result<DataCategoriesResponse> {
71        if !soql::is_safe_field_name(group) {
72            return Err(Error::new(ErrorKind::Salesforce {
73                error_code: "INVALID_GROUP".to_string(),
74                message: "Invalid data category group name".to_string(),
75            }));
76        }
77        if let Some(s) = sobject {
78            if !soql::is_safe_sobject_name(s) {
79                return Err(Error::new(ErrorKind::Salesforce {
80                    error_code: "INVALID_SOBJECT".to_string(),
81                    message: "Invalid SObject name".to_string(),
82                }));
83            }
84        }
85        let path = match sobject {
86            Some(s) => format!(
87                "support/dataCategoryGroups/{}/dataCategories?sObjectType={}",
88                group, s
89            ),
90            None => format!("support/dataCategoryGroups/{}/dataCategories", group),
91        };
92        self.client.rest_get(&path).await.map_err(Into::into)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::super::SalesforceRestClient;
99
100    #[tokio::test]
101    async fn test_data_category_groups_invalid_sobject() {
102        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
103        let result = client.data_category_groups(Some("Bad'; DROP--")).await;
104        assert!(result.is_err());
105        assert!(result.unwrap_err().to_string().contains("INVALID_SOBJECT"));
106    }
107
108    #[tokio::test]
109    async fn test_data_categories_invalid_group() {
110        let client = SalesforceRestClient::new("https://test.salesforce.com", "token").unwrap();
111        let result = client.data_categories("Bad'; DROP--", None).await;
112        assert!(result.is_err());
113        assert!(result.unwrap_err().to_string().contains("INVALID_GROUP"));
114    }
115
116    #[tokio::test]
117    async fn test_knowledge_settings_wiremock() {
118        use wiremock::matchers::{method, path_regex};
119        use wiremock::{Mock, MockServer, ResponseTemplate};
120
121        let mock_server = MockServer::start().await;
122
123        let body = serde_json::json!({
124            "defaultLanguage": "en_US",
125            "knowledgeEnabled": true,
126            "languages": []
127        });
128
129        Mock::given(method("GET"))
130            .and(path_regex(".*/knowledgeManagement/settings$"))
131            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
132            .mount(&mock_server)
133            .await;
134
135        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
136        let result = client
137            .knowledge_settings()
138            .await
139            .expect("knowledge_settings should succeed");
140        assert!(result.knowledge_enabled);
141    }
142
143    #[tokio::test]
144    async fn test_knowledge_articles_wiremock() {
145        use wiremock::matchers::{method, path_regex};
146        use wiremock::{Mock, MockServer, ResponseTemplate};
147
148        let mock_server = MockServer::start().await;
149
150        let body = serde_json::json!({
151            "articles": [{
152                "id": "kA0xx0000000001",
153                "articleNumber": "000001",
154                "title": "Test Article",
155                "urlName": "test-article",
156                "summary": "A test article"
157            }],
158            "currentPageUrl": null,
159            "nextPageUrl": null,
160            "pageNumber": 1
161        });
162
163        Mock::given(method("GET"))
164            .and(path_regex(".*/support/knowledgeArticles.*"))
165            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
166            .mount(&mock_server)
167            .await;
168
169        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
170        let result = client
171            .knowledge_articles(Some("test"), None)
172            .await
173            .expect("knowledge_articles should succeed");
174        assert_eq!(result.articles.len(), 1);
175    }
176
177    #[tokio::test]
178    async fn test_data_category_groups_wiremock() {
179        use wiremock::matchers::{method, path_regex};
180        use wiremock::{Mock, MockServer, ResponseTemplate};
181
182        let mock_server = MockServer::start().await;
183
184        let body = serde_json::json!({
185            "categoryGroups": [{
186                "name": "Products",
187                "label": "Products",
188                "objectUsage": null,
189                "topCategoriesUrl": null
190            }]
191        });
192
193        Mock::given(method("GET"))
194            .and(path_regex(".*/support/dataCategoryGroups$"))
195            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
196            .mount(&mock_server)
197            .await;
198
199        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
200        let result = client
201            .data_category_groups(None)
202            .await
203            .expect("data_category_groups should succeed");
204        assert_eq!(result.category_groups.len(), 1);
205    }
206
207    #[tokio::test]
208    async fn test_data_categories_wiremock() {
209        use wiremock::matchers::{method, path_regex};
210        use wiremock::{Mock, MockServer, ResponseTemplate};
211
212        let mock_server = MockServer::start().await;
213
214        let body = serde_json::json!({
215            "categories": [{
216                "name": "Software",
217                "label": "Software",
218                "url": null,
219                "childCategories": []
220            }]
221        });
222
223        Mock::given(method("GET"))
224            .and(path_regex(
225                ".*/support/dataCategoryGroups/Products/dataCategories.*",
226            ))
227            .respond_with(ResponseTemplate::new(200).set_body_json(&body))
228            .mount(&mock_server)
229            .await;
230
231        let client = SalesforceRestClient::new(mock_server.uri(), "test-token").unwrap();
232        let result = client
233            .data_categories("Products", None)
234            .await
235            .expect("data_categories should succeed");
236        assert_eq!(result.categories.len(), 1);
237    }
238}