busbar_sf_rest/client/
knowledge.rs1use 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 #[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 #[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(¶ms.join("&"));
39 }
40 self.client.rest_get(&path).await.map_err(Into::into)
41 }
42
43 #[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 #[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}