1use chrono::Duration;
37use reqwest::{
38 Url,
39 header::{HeaderMap, HeaderName, HeaderValue},
40};
41use serde::{Deserialize, Serialize};
42
43mod auth;
44mod builders;
45mod client;
46mod error;
47
48pub use crate::auth::Credentials;
49pub use builders::{
50 NoteBuilder, TicketClient, TicketCreateBuilder, TicketSearchBuilder, TicketStatus,
51 TicketsClient,
52};
53pub use client::{
54 Attachment, Condition, CreateTicketData, Criteria, DetailedTicket, EditTicketData, LogicalOp,
55 NameWrapper, Note, NoteData, NoteResponse, Priority, Resolution, SizeInfo, Status, TicketData,
56 TicketResponse, TimeEntry, UserInfo,
57};
58pub use error::{Error, SdpErrorCode};
59
60#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Default)]
62pub struct UserID(pub String);
63
64#[derive(Clone, Debug)]
66pub struct TicketID(pub u64);
67
68#[derive(Clone, Debug)]
70pub struct NoteID(pub u64);
71
72impl From<u64> for NoteID {
73 fn from(value: u64) -> Self {
74 NoteID(value)
75 }
76}
77
78impl From<NoteID> for u64 {
79 fn from(value: NoteID) -> Self {
80 value.0
81 }
82}
83
84impl std::fmt::Display for NoteID {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 write!(f, "{}", self.0)
87 }
88}
89
90impl From<u64> for TicketID {
91 fn from(value: u64) -> Self {
92 TicketID(value)
93 }
94}
95
96impl From<TicketID> for u64 {
97 fn from(value: TicketID) -> Self {
98 value.0
99 }
100}
101
102impl From<&TicketID> for u64 {
103 fn from(value: &TicketID) -> Self {
104 value.0
105 }
106}
107
108impl From<&UserID> for String {
109 fn from(value: &UserID) -> Self {
110 value.0.clone()
111 }
112}
113
114impl From<String> for UserID {
115 fn from(value: String) -> Self {
116 UserID(value)
117 }
118}
119
120impl From<&str> for UserID {
121 fn from(value: &str) -> Self {
122 UserID(value.to_string())
123 }
124}
125
126impl From<u32> for UserID {
127 fn from(value: u32) -> Self {
128 UserID(value.to_string())
129 }
130}
131
132impl From<UserID> for u32 {
133 fn from(value: UserID) -> Self {
134 value.0.parse().unwrap_or_default()
135 }
136}
137
138impl std::fmt::Display for TicketID {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 write!(f, "{}", self.0)
141 }
142}
143
144impl std::fmt::Display for UserID {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(f, "{}", self.0)
147 }
148}
149
150#[derive(Clone)]
155pub struct ServiceDesk {
156 pub base_url: Url,
157 pub credentials: Credentials,
158 inner: reqwest::Client,
159}
160
161#[derive(Clone, Debug)]
164pub enum Security {
165 Unsafe,
166 NativeTlS,
167}
168
169#[derive(Clone, Debug)]
171pub struct ServiceDeskOptions {
172 user_agent: Option<String>,
173 timeout: Option<Duration>,
175 security: Option<Security>,
176 default_headers: Option<HeaderMap>,
177}
178
179static SDP_HEADER: (HeaderName, HeaderValue) = (
180 HeaderName::from_static("accept"),
181 HeaderValue::from_static("application/vnd.manageengine.sdp.v3+json"),
182);
183
184impl Default for ServiceDeskOptions {
185 fn default() -> Self {
186 ServiceDeskOptions {
187 user_agent: Some(String::from("servicedesk-rs/0.1.0")),
188 timeout: Some(Duration::seconds(5)),
189 security: Some(Security::Unsafe),
190 default_headers: Some(HeaderMap::from_iter(vec![SDP_HEADER.clone()])),
191 }
192 }
193}
194
195impl ServiceDesk {
196 pub fn new(base_url: Url, credentials: Credentials, options: ServiceDeskOptions) -> Self {
198 let mut headers = options.default_headers.unwrap_or_default();
199
200 #[allow(clippy::single_match)]
201 match credentials {
202 Credentials::Token { ref token } => {
203 headers.insert("authtoken", HeaderValue::from_str(token).unwrap());
204 }
205 _ => {}
206 }
207 let mut inner = reqwest::ClientBuilder::new()
208 .default_headers(headers)
209 .user_agent(options.user_agent.unwrap_or_default())
210 .timeout(options.timeout.unwrap_or_default().to_std().unwrap());
211
212 if let Some(security) = options.security {
213 match security {
214 Security::Unsafe => {
215 inner = inner.danger_accept_invalid_certs(true);
216 }
217 Security::NativeTlS => {
218 }
220 }
221 };
222
223 let inner = inner.build().expect("failed to build sdp client");
224
225 ServiceDesk {
226 base_url,
227 credentials,
228 inner,
229 }
230 }
231}
232
233#[cfg(test)]
234mod test {
235 use super::*;
236 use crate::client::{EditTicketData, NameWrapper};
237
238 pub fn setup() -> ServiceDesk {
240 dotenv::dotenv().ok();
241 let token = std::env::var("SDP_TEST_TOKEN").expect("SDP_TEST_TOKEN must be set");
242 let url = std::env::var("SDP_TEST_URL").expect("SDP_TEST_URL must be set");
243
244 let creds = Credentials::Token { token };
245
246 ServiceDesk::new(
247 Url::parse(&url).unwrap(),
248 creds,
249 ServiceDeskOptions::default(),
250 )
251 }
252
253 #[tokio::test]
254 async fn builder_ticket_get() {
255 let sdp = setup();
256 let result = sdp.ticket(65997).get().await;
257 assert!(result.is_ok());
258 let ticket = result.unwrap();
259 assert_eq!(ticket.id, "65997");
260 }
261
262 #[tokio::test]
263 async fn builder_search_open_tickets() {
264 let sdp = setup();
265 let result = sdp
266 .tickets()
267 .search()
268 .open()
269 .subject_contains("First")
270 .limit(10)
271 .fetch()
272 .await;
273 assert!(result.is_ok());
274 }
275
276 #[tokio::test]
277 async fn builder_search_by_alert_id() {
278 let sdp = setup();
279 let result = sdp
280 .tickets()
281 .search()
282 .field_equals(
283 "udf_fields.udf_mline_1202",
284 "23433465d4e0ee849a49b994a27a8bbdad726686b73623aebedeef5b69ec1fb2",
285 )
286 .first()
287 .await;
288 assert!(result.is_ok());
289 let result = result.unwrap();
290 assert!(result.is_some());
291 }
292
293 #[tokio::test]
294 async fn builder_create_ticket() {
295 let sdp = setup();
296 let result = sdp
297 .tickets()
298 .create()
299 .subject("[TEST] Test Builder API")
300 .description("Created via builder pattern")
301 .requester("NETXP")
302 .priority("Low")
303 .account("SOC - NETXP")
304 .template("SOC-with-alert-id")
305 .send()
306 .await;
307 assert!(result.is_ok());
308 }
309
310 #[tokio::test]
311 async fn builder_add_note() {
312 let sdp = setup();
313 let result = sdp
314 .ticket(65997)
315 .add_note("Note added via builder API")
316 .await;
317 assert!(result.is_ok());
318 }
319
320 #[tokio::test]
321 async fn builder_note_with_options() {
322 let sdp = setup();
323 let result = sdp
324 .ticket(65997)
325 .note()
326 .description("Note with options via builder")
327 .show_to_requester()
328 .send()
329 .await;
330 assert!(result.is_ok());
331 }
332
333 #[tokio::test]
334 async fn builder_assign_ticket() {
335 let sdp = setup();
336 let result = sdp.ticket(250225).assign("Szymon Głuch").await;
337 assert!(result.is_ok());
338 }
339
340 #[tokio::test]
341 async fn builder_edit_ticket() {
342 let sdp = setup();
343 let editdata = EditTicketData {
344 subject: "Updated via builder".to_string(),
345 description: None,
346 requester: Some(NameWrapper {
347 name: "GALLUP".to_string(),
348 }),
349 priority: Some(NameWrapper {
350 name: "High".to_string(),
351 }),
352 udf_fields: None,
353 };
354
355 let result = sdp.ticket(250225).edit(&editdata).await;
356 assert!(result.is_ok());
357 }
358
359 #[tokio::test]
360 async fn builder_list_notes() {
361 let sdp = setup();
362 let result = sdp.list_notes(250225, None, None).await;
363 assert!(result.is_ok());
364 let notes = result.unwrap();
365 assert_eq!(
366 (notes[0].id.clone(), notes[1].id.clone()),
367 ("279486".to_string(), "279666".to_string())
368 )
369 }
370
371 #[tokio::test]
372 async fn builder_get_note() {
373 let sdp = setup();
374 let result = sdp.get_note(250225, 279486).await;
375 assert!(result.is_ok());
376 let note = result.unwrap();
377 assert_eq!(note.description, "<div>test note<br></div>");
378 }
379
380 #[tokio::test]
381 async fn builder_create_delete_note() {
382 let sdp = setup();
383 let create_result = sdp
384 .ticket(250225)
385 .note()
386 .description("Note to be deleted")
387 .send()
388 .await;
389 assert!(create_result.is_ok());
390 let created_note = create_result.unwrap();
391
392 let delete_result = sdp
393 .delete_note(250225, created_note.id.parse::<u64>().unwrap())
394 .await;
395 assert!(delete_result.is_ok());
396 }
397}