1use derive_builder::Builder;
17use reqwest::Method;
18use std::borrow::Cow;
19
20use crate::api::custom_fields::CustomFieldEssentialsWithValue;
21use crate::api::groups::GroupEssentials;
22use crate::api::project_memberships::UserProjectMembership;
23use crate::api::{Endpoint, NoPagination, Pageable, QueryParams, ReturnsJsonResponse};
24use serde::Serialize;
25
26#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29pub struct UserEssentials {
30 pub id: u64,
32 pub name: String,
34}
35
36#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
40pub struct User {
41 pub id: u64,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub status: Option<u64>,
48 pub login: String,
50 pub admin: bool,
52 pub firstname: String,
54 pub lastname: String,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub mail: Option<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub api_key: Option<String>,
62 #[serde(default)]
64 pub twofa_scheme: Option<String>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 auth_source_id: Option<u64>,
68 #[serde(
70 serialize_with = "crate::api::serialize_rfc3339",
71 deserialize_with = "crate::api::deserialize_rfc3339"
72 )]
73 pub created_on: time::OffsetDateTime,
74 #[serde(
76 serialize_with = "crate::api::serialize_rfc3339",
77 deserialize_with = "crate::api::deserialize_rfc3339"
78 )]
79 pub updated_on: time::OffsetDateTime,
80 #[serde(
82 serialize_with = "crate::api::serialize_optional_rfc3339",
83 deserialize_with = "crate::api::deserialize_optional_rfc3339"
84 )]
85 pub passwd_changed_on: Option<time::OffsetDateTime>,
86 #[serde(
88 serialize_with = "crate::api::serialize_optional_rfc3339",
89 deserialize_with = "crate::api::deserialize_optional_rfc3339"
90 )]
91 pub last_login_on: Option<time::OffsetDateTime>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub groups: Option<Vec<GroupEssentials>>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub memberships: Option<Vec<UserProjectMembership>>,
101}
102
103#[derive(Debug, Clone)]
105pub enum UserStatus {
106 Active,
108 Registered,
110 Locked,
112 AnyStatus,
114}
115
116impl std::fmt::Display for UserStatus {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 match self {
119 Self::Active => {
120 write!(f, "Active")
121 }
122 Self::Registered => {
123 write!(f, "Registered")
124 }
125 Self::Locked => {
126 write!(f, "Locked")
127 }
128 Self::AnyStatus => {
129 write!(f, "")
130 }
131 }
132 }
133}
134
135#[derive(Debug, Clone, Builder)]
137#[builder(setter(strip_option))]
138pub struct ListUsers<'a> {
139 #[builder(default)]
141 status: Option<UserStatus>,
143 #[builder(default)]
144 #[builder(setter(into))]
146 name: Option<Cow<'a, str>>,
147 #[builder(default)]
149 group_id: Option<u64>,
150}
151
152impl ReturnsJsonResponse for ListUsers<'_> {}
153impl Pageable for ListUsers<'_> {
154 fn response_wrapper_key(&self) -> String {
155 "users".to_string()
156 }
157}
158
159impl<'a> ListUsers<'a> {
160 #[must_use]
162 pub fn builder() -> ListUsersBuilder<'a> {
163 ListUsersBuilder::default()
164 }
165}
166
167impl Endpoint for ListUsers<'_> {
168 fn method(&self) -> Method {
169 Method::GET
170 }
171
172 fn endpoint(&self) -> Cow<'static, str> {
173 "users.json".into()
174 }
175
176 fn parameters(&self) -> QueryParams {
177 let mut params = QueryParams::default();
178 params.push_opt("status", self.status.as_ref().map(|s| s.to_string()));
179 params.push_opt("name", self.name.as_ref());
180 params.push_opt("group_id", self.group_id);
181 params
182 }
183}
184
185#[derive(Debug, Clone)]
187pub enum UserInclude {
188 Memberships,
190 Groups,
192}
193
194impl std::fmt::Display for UserInclude {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 match self {
197 Self::Memberships => {
198 write!(f, "memberships")
199 }
200 Self::Groups => {
201 write!(f, "groups")
202 }
203 }
204 }
205}
206
207#[derive(Debug, Clone, Builder)]
209#[builder(setter(strip_option))]
210pub struct GetUser {
211 #[builder(default)]
213 id: Option<u64>,
214 #[builder(default)]
216 include: Option<Vec<UserInclude>>,
217}
218
219impl ReturnsJsonResponse for GetUser {}
220impl NoPagination for GetUser {}
221
222impl GetUser {
223 #[must_use]
225 pub fn builder() -> GetUserBuilder {
226 GetUserBuilder::default()
227 }
228}
229
230impl Endpoint for GetUser {
231 fn method(&self) -> Method {
232 Method::GET
233 }
234
235 fn endpoint(&self) -> Cow<'static, str> {
236 match self.id {
237 Some(id) => format!("users/{}.json", id).into(),
238 None => "users/current.json".into(),
239 }
240 }
241
242 fn parameters(&self) -> QueryParams {
243 let mut params = QueryParams::default();
244 params.push_opt("include", self.include.as_ref());
245 params
246 }
247}
248
249#[derive(Debug, Clone, Serialize)]
251#[serde(rename_all = "snake_case")]
252pub enum MailNotificationOptions {
253 All,
255 Selected,
257 OnlyMyEvents,
259 OnlyAssigned,
261 OnlyOwner,
263 #[serde(rename = "none")]
265 NoMailNotifications,
266}
267
268#[serde_with::skip_serializing_none]
270#[derive(Debug, Clone, Builder, Serialize)]
271#[builder(setter(strip_option))]
272pub struct CreateUser<'a> {
273 #[builder(setter(into))]
275 login: Cow<'a, str>,
276 #[builder(setter(into), default)]
280 password: Option<Cow<'a, str>>,
281 #[builder(setter(into))]
283 firstname: Cow<'a, str>,
284 #[builder(setter(into))]
286 lastname: Cow<'a, str>,
287 #[builder(setter(into))]
289 mail: Cow<'a, str>,
290 #[builder(default)]
292 auth_source_id: Option<u64>,
293 #[builder(default)]
295 mail_notification: Option<MailNotificationOptions>,
296 #[builder(default)]
298 must_change_passwd: Option<bool>,
299 #[builder(default)]
301 generate_password: Option<bool>,
302 #[builder(default)]
304 #[serde(skip_serializing)]
305 send_information: Option<bool>,
306 #[builder(default)]
308 admin: Option<bool>,
309}
310
311impl ReturnsJsonResponse for CreateUser<'_> {}
312impl NoPagination for CreateUser<'_> {}
313
314impl<'a> CreateUser<'a> {
315 #[must_use]
317 pub fn builder() -> CreateUserBuilder<'a> {
318 CreateUserBuilder::default()
319 }
320}
321
322impl Endpoint for CreateUser<'_> {
323 fn method(&self) -> Method {
324 Method::POST
325 }
326
327 fn endpoint(&self) -> Cow<'static, str> {
328 "users.json".into()
329 }
330
331 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
332 Ok(Some((
333 "application/json",
334 serde_json::to_vec(&UserWrapperWithSendInformation::<CreateUser> {
335 user: (*self).to_owned(),
336 send_information: self.send_information,
337 })?,
338 )))
339 }
340}
341
342#[serde_with::skip_serializing_none]
344#[derive(Debug, Clone, Builder, Serialize)]
345#[builder(setter(strip_option))]
346pub struct UpdateUser<'a> {
347 #[serde(skip_serializing)]
349 id: u64,
350 #[builder(setter(into))]
352 login: Cow<'a, str>,
353 #[builder(setter(into), default)]
357 password: Option<Cow<'a, str>>,
358 #[builder(default, setter(into))]
360 firstname: Option<Cow<'a, str>>,
361 #[builder(default, setter(into))]
363 lastname: Option<Cow<'a, str>>,
364 #[builder(default, setter(into))]
366 mail: Option<Cow<'a, str>>,
367 #[builder(default)]
369 auth_source_id: Option<u64>,
370 #[builder(default)]
372 mail_notification: Option<MailNotificationOptions>,
373 #[builder(default)]
375 must_change_passwd: Option<bool>,
376 #[builder(default)]
378 generate_password: Option<bool>,
379 #[builder(default)]
381 #[serde(skip_serializing)]
382 send_information: Option<bool>,
383 #[builder(default)]
385 admin: Option<bool>,
386}
387
388impl<'a> UpdateUser<'a> {
389 #[must_use]
391 pub fn builder() -> UpdateUserBuilder<'a> {
392 UpdateUserBuilder::default()
393 }
394}
395
396impl Endpoint for UpdateUser<'_> {
397 fn method(&self) -> Method {
398 Method::PUT
399 }
400
401 fn endpoint(&self) -> Cow<'static, str> {
402 format!("users/{}.json", self.id).into()
403 }
404
405 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
406 Ok(Some((
407 "application/json",
408 serde_json::to_vec(&UserWrapperWithSendInformation::<UpdateUser> {
409 user: (*self).to_owned(),
410 send_information: self.send_information,
411 })?,
412 )))
413 }
414}
415
416#[derive(Debug, Clone, Builder)]
418#[builder(setter(strip_option))]
419pub struct DeleteUser {
420 id: u64,
422}
423
424impl DeleteUser {
425 #[must_use]
427 pub fn builder() -> DeleteUserBuilder {
428 DeleteUserBuilder::default()
429 }
430}
431
432impl Endpoint for DeleteUser {
433 fn method(&self) -> Method {
434 Method::DELETE
435 }
436
437 fn endpoint(&self) -> Cow<'static, str> {
438 format!("users/{}.json", &self.id).into()
439 }
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
444pub struct UsersWrapper<T> {
445 pub users: Vec<T>,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
452pub struct UserWrapper<T> {
453 pub user: T,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
461pub struct UserWrapperWithSendInformation<T> {
462 pub user: T,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
466 pub send_information: Option<bool>,
467}
468
469#[cfg(test)]
470mod test {
471 use crate::api::ResponsePage;
472
473 use super::*;
474 use pretty_assertions::assert_eq;
475 use std::error::Error;
476 use tokio::sync::RwLock;
477 use tracing_test::traced_test;
478
479 static USER_LOCK: RwLock<()> = RwLock::const_new(());
482
483 #[traced_test]
484 #[test]
485 fn test_list_users_first_page() -> Result<(), Box<dyn Error>> {
486 let _r_user = USER_LOCK.read();
487 dotenvy::dotenv()?;
488 let redmine = crate::api::Redmine::from_env()?;
489 let endpoint = ListUsers::builder().build()?;
490 redmine.json_response_body_page::<_, User>(&endpoint, 0, 25)?;
491 Ok(())
492 }
493
494 #[traced_test]
495 #[test]
496 fn test_list_users_all_pages() -> Result<(), Box<dyn Error>> {
497 let _r_user = USER_LOCK.read();
498 dotenvy::dotenv()?;
499 let redmine = crate::api::Redmine::from_env()?;
500 let endpoint = ListUsers::builder().build()?;
501 redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
502 Ok(())
503 }
504
505 #[traced_test]
506 #[test]
507 fn test_get_user() -> Result<(), Box<dyn Error>> {
508 let _r_user = USER_LOCK.read();
509 dotenvy::dotenv()?;
510 let redmine = crate::api::Redmine::from_env()?;
511 let endpoint = GetUser::builder().id(1).build()?;
512 redmine.json_response_body::<_, UserWrapper<User>>(&endpoint)?;
513 Ok(())
514 }
515
516 #[function_name::named]
517 #[traced_test]
518 #[test]
519 fn test_create_user() -> Result<(), Box<dyn Error>> {
520 let _w_user = USER_LOCK.write();
521 let name = format!("unittest_{}", function_name!());
522 dotenvy::dotenv()?;
523 let redmine = crate::api::Redmine::from_env()?;
524 let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
525 let users: Vec<User> = redmine.json_response_body_all_pages(&list_endpoint)?;
526 for user in users {
527 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
528 redmine.ignore_response_body::<_>(&delete_endpoint)?;
529 }
530 let create_endpoint = CreateUser::builder()
531 .login(name.clone())
532 .firstname("Unit")
533 .lastname("Test")
534 .mail(format!("unit-test_{}@example.org", name))
535 .build()?;
536 let UserWrapper { user } =
537 redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
538 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
539 redmine.ignore_response_body::<_>(&delete_endpoint)?;
540 Ok(())
541 }
542
543 #[function_name::named]
610 #[traced_test]
611 #[test]
612 fn test_update_user() -> Result<(), Box<dyn Error>> {
613 let _w_user = USER_LOCK.write();
614 let name = format!("unittest_{}", function_name!());
615 dotenvy::dotenv()?;
616 let redmine = crate::api::Redmine::from_env()?;
617 let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
618 let users: Vec<User> = redmine.json_response_body_all_pages(&list_endpoint)?;
619 for user in users {
620 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
621 redmine.ignore_response_body::<_>(&delete_endpoint)?;
622 }
623 let create_endpoint = CreateUser::builder()
624 .login(name.clone())
625 .firstname("Unit")
626 .lastname("Test")
627 .mail(format!("unit-test_{}@example.org", name))
628 .build()?;
629 let UserWrapper { user } =
630 redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
631 let update_endpoint = super::UpdateUser::builder()
632 .id(user.id)
633 .login(format!("new_{}", name))
634 .build()?;
635 redmine.ignore_response_body::<_>(&update_endpoint)?;
636 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
637 redmine.ignore_response_body::<_>(&delete_endpoint)?;
638 Ok(())
639 }
640
641 #[traced_test]
646 #[test]
647 fn test_completeness_user_type_first_page() -> Result<(), Box<dyn Error>> {
648 let _r_user = USER_LOCK.read();
649 dotenvy::dotenv()?;
650 let redmine = crate::api::Redmine::from_env()?;
651 let endpoint = ListUsers::builder().build()?;
652 let ResponsePage {
653 values,
654 total_count: _,
655 offset: _,
656 limit: _,
657 } = redmine.json_response_body_page::<_, serde_json::Value>(&endpoint, 0, 100)?;
658 for value in values {
659 let o: User = serde_json::from_value(value.clone())?;
660 let reserialized = serde_json::to_value(o)?;
661 assert_eq!(value, reserialized);
662 }
663 Ok(())
664 }
665
666 #[traced_test]
674 #[test]
675 fn test_completeness_user_type_all_pages_all_user_details() -> Result<(), Box<dyn Error>> {
676 let _r_user = USER_LOCK.read();
677 dotenvy::dotenv()?;
678 let redmine = crate::api::Redmine::from_env()?;
679 let endpoint = ListUsers::builder().build()?;
680 let users = redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
681 for user in users {
682 let get_endpoint = GetUser::builder()
683 .id(user.id)
684 .include(vec![UserInclude::Memberships, UserInclude::Groups])
685 .build()?;
686 let UserWrapper { user: value } =
687 redmine.json_response_body::<_, UserWrapper<serde_json::Value>>(&get_endpoint)?;
688 let o: User = serde_json::from_value(value.clone())?;
689 let reserialized = serde_json::to_value(o)?;
690 assert_eq!(value, reserialized);
691 }
692 Ok(())
693 }
694}