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::{
24 CustomFieldFilter, DateTimeFilterPast, Endpoint, NoPagination, Pageable, QueryParams,
25 ReturnsJsonResponse,
26};
27use serde::Serialize;
28
29#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub struct UserEssentials {
33 pub id: u64,
35 pub name: String,
37}
38
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
43pub struct User {
44 pub id: u64,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub status: Option<UserStatus>,
49 pub login: String,
51 pub admin: bool,
53 pub firstname: String,
55 pub lastname: String,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub mail: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub api_key: Option<String>,
63 #[serde(default)]
65 pub twofa_scheme: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 auth_source_id: Option<u64>,
69 #[serde(
71 serialize_with = "crate::api::serialize_rfc3339",
72 deserialize_with = "crate::api::deserialize_rfc3339"
73 )]
74 pub created_on: time::OffsetDateTime,
75 #[serde(
77 serialize_with = "crate::api::serialize_rfc3339",
78 deserialize_with = "crate::api::deserialize_rfc3339"
79 )]
80 pub updated_on: time::OffsetDateTime,
81 #[serde(
83 serialize_with = "crate::api::serialize_optional_rfc3339",
84 deserialize_with = "crate::api::deserialize_optional_rfc3339"
85 )]
86 pub passwd_changed_on: Option<time::OffsetDateTime>,
87 #[serde(
89 serialize_with = "crate::api::serialize_optional_rfc3339",
90 deserialize_with = "crate::api::deserialize_optional_rfc3339"
91 )]
92 pub last_login_on: Option<time::OffsetDateTime>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub groups: Option<Vec<GroupEssentials>>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub memberships: Option<Vec<UserProjectMembership>>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum UserStatus {
107 Active,
109 Registered,
111 Locked,
113}
114
115impl serde::Serialize for UserStatus {
116 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
117 where
118 S: serde::Serializer,
119 {
120 match self {
121 Self::Active => serializer.serialize_u64(1),
122 Self::Registered => serializer.serialize_u64(2),
123 Self::Locked => serializer.serialize_u64(3),
124 }
125 }
126}
127
128impl<'de> serde::Deserialize<'de> for UserStatus {
129 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
130 where
131 D: serde::Deserializer<'de>,
132 {
133 let status_code = u64::deserialize(deserializer)?;
134 match status_code {
135 1 => Ok(Self::Active),
136 2 => Ok(Self::Registered),
137 3 => Ok(Self::Locked),
138 _ => Err(serde::de::Error::custom(format!(
139 "unknown user status code: {status_code}"
140 ))),
141 }
142 }
143}
144
145#[derive(Debug, Clone)]
147pub enum UserStatusFilter {
148 Active,
150 Registered,
152 Locked,
154 Any,
156}
157
158impl std::fmt::Display for UserStatusFilter {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self {
161 Self::Active => {
162 write!(f, "1")
163 }
164 Self::Registered => {
165 write!(f, "2")
166 }
167 Self::Locked => {
168 write!(f, "3")
169 }
170 Self::Any => {
171 write!(f, "*")
172 }
173 }
174 }
175}
176
177impl From<UserStatus> for UserStatusFilter {
178 fn from(value: UserStatus) -> Self {
179 match value {
180 UserStatus::Active => Self::Active,
181 UserStatus::Registered => Self::Registered,
182 UserStatus::Locked => Self::Locked,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Builder)]
189#[builder(setter(strip_option))]
190pub struct ListUsers<'a> {
191 #[builder(default)]
193 status: Option<UserStatusFilter>,
194 #[builder(default)]
195 #[builder(setter(into))]
197 name: Option<Cow<'a, str>>,
198 #[builder(default)]
200 group_id: Option<u64>,
201 #[builder(default)]
203 auth_source_id: Option<u64>,
204 #[builder(default)]
206 #[builder(setter(into))]
207 twofa_scheme: Option<Cow<'a, str>>,
208 #[builder(default)]
210 admin: Option<bool>,
211 #[builder(default)]
213 created_on: Option<DateTimeFilterPast>,
214 #[builder(default)]
216 last_login_on: Option<DateTimeFilterPast>,
217 #[builder(default)]
219 #[builder(setter(into))]
220 login: Option<Cow<'a, str>>,
221 #[builder(default)]
223 #[builder(setter(into))]
224 firstname: Option<Cow<'a, str>>,
225 #[builder(default)]
227 #[builder(setter(into))]
228 lastname: Option<Cow<'a, str>>,
229 #[builder(default)]
231 #[builder(setter(into))]
232 mail: Option<Cow<'a, str>>,
233 #[builder(default)]
235 custom_field_filters: Option<Vec<CustomFieldFilter>>,
236}
237
238impl ReturnsJsonResponse for ListUsers<'_> {}
239impl Pageable for ListUsers<'_> {
240 fn response_wrapper_key(&self) -> String {
241 "users".to_string()
242 }
243}
244
245impl<'a> ListUsers<'a> {
246 #[must_use]
248 pub fn builder() -> ListUsersBuilder<'a> {
249 ListUsersBuilder::default()
250 }
251}
252
253impl Endpoint for ListUsers<'_> {
254 fn method(&self) -> Method {
255 Method::GET
256 }
257
258 fn endpoint(&self) -> Cow<'static, str> {
259 "users.json".into()
260 }
261
262 fn parameters(&self) -> QueryParams<'_> {
263 let mut params = QueryParams::default();
264 params.push_opt("status", self.status.as_ref().map(|s| s.to_string()));
265 params.push_opt("name", self.name.as_ref());
266 params.push_opt("group_id", self.group_id);
267 params.push_opt("auth_source_id", self.auth_source_id);
268 params.push_opt("twofa_scheme", self.twofa_scheme.as_ref());
269 params.push_opt("admin", self.admin);
270 params.push_opt(
271 "created_on",
272 self.created_on.as_ref().map(|s| s.to_string()),
273 );
274 params.push_opt(
275 "last_login_on",
276 self.last_login_on.as_ref().map(|s| s.to_string()),
277 );
278 params.push_opt("login", self.login.as_ref());
279 params.push_opt("firstname", self.firstname.as_ref());
280 params.push_opt("lastname", self.lastname.as_ref());
281 params.push_opt("mail", self.mail.as_ref());
282 if let Some(filters) = self.custom_field_filters.as_ref() {
283 for filter in filters {
284 params.push(format!("cf_{}", filter.id), filter.value.to_string());
285 }
286 }
287 params
288 }
289}
290
291#[derive(Debug, Clone)]
293pub enum UserInclude {
294 Memberships,
296 Groups,
298}
299
300impl std::fmt::Display for UserInclude {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 match self {
303 Self::Memberships => {
304 write!(f, "memberships")
305 }
306 Self::Groups => {
307 write!(f, "groups")
308 }
309 }
310 }
311}
312
313#[derive(Debug, Clone, Builder)]
315#[builder(setter(strip_option))]
316pub struct GetUser {
317 #[builder(default)]
319 id: Option<u64>,
320 #[builder(default)]
322 include: Option<Vec<UserInclude>>,
323}
324
325impl ReturnsJsonResponse for GetUser {}
326impl NoPagination for GetUser {}
327
328impl GetUser {
329 #[must_use]
331 pub fn builder() -> GetUserBuilder {
332 GetUserBuilder::default()
333 }
334}
335
336impl Endpoint for GetUser {
337 fn method(&self) -> Method {
338 Method::GET
339 }
340
341 fn endpoint(&self) -> Cow<'static, str> {
342 match self.id {
343 Some(id) => format!("users/{id}.json").into(),
344 None => "users/current.json".into(),
345 }
346 }
347
348 fn parameters(&self) -> QueryParams<'_> {
349 let mut params = QueryParams::default();
350 params.push_opt("include", self.include.as_ref());
351 params
352 }
353}
354
355#[derive(Debug, Clone, Serialize)]
357#[serde(rename_all = "snake_case")]
358pub enum MailNotificationOptions {
359 All,
361 Selected,
363 OnlyMyEvents,
365 OnlyAssigned,
367 OnlyOwner,
369 #[serde(rename = "none")]
371 NoMailNotifications,
372}
373
374#[serde_with::skip_serializing_none]
376#[derive(Debug, Clone, Builder, Serialize)]
377#[builder(setter(strip_option))]
378pub struct CreateUser<'a> {
379 #[builder(setter(into))]
381 login: Cow<'a, str>,
382 #[builder(setter(into), default)]
386 password: Option<Cow<'a, str>>,
387 #[builder(setter(into))]
389 firstname: Cow<'a, str>,
390 #[builder(setter(into))]
392 lastname: Cow<'a, str>,
393 #[builder(setter(into))]
395 mail: Cow<'a, str>,
396 #[builder(default)]
398 auth_source_id: Option<u64>,
399 #[builder(default)]
401 mail_notification: Option<MailNotificationOptions>,
402 #[builder(default)]
404 must_change_passwd: Option<bool>,
405 #[builder(default)]
407 generate_password: Option<bool>,
408 #[builder(default)]
410 #[serde(skip_serializing)]
411 send_information: Option<bool>,
412 #[builder(default)]
414 admin: Option<bool>,
415}
416
417impl ReturnsJsonResponse for CreateUser<'_> {}
418impl NoPagination for CreateUser<'_> {}
419
420impl<'a> CreateUser<'a> {
421 #[must_use]
423 pub fn builder() -> CreateUserBuilder<'a> {
424 CreateUserBuilder::default()
425 }
426}
427
428impl Endpoint for CreateUser<'_> {
429 fn method(&self) -> Method {
430 Method::POST
431 }
432
433 fn endpoint(&self) -> Cow<'static, str> {
434 "users.json".into()
435 }
436
437 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
438 Ok(Some((
439 "application/json",
440 serde_json::to_vec(&UserWrapperWithSendInformation::<CreateUser> {
441 user: (*self).to_owned(),
442 send_information: self.send_information,
443 })?,
444 )))
445 }
446}
447
448#[serde_with::skip_serializing_none]
450#[derive(Debug, Clone, Builder, Serialize)]
451#[builder(setter(strip_option))]
452pub struct UpdateUser<'a> {
453 #[serde(skip_serializing)]
455 id: u64,
456 #[builder(setter(into))]
458 login: Cow<'a, str>,
459 #[builder(setter(into), default)]
463 password: Option<Cow<'a, str>>,
464 #[builder(default, setter(into))]
466 firstname: Option<Cow<'a, str>>,
467 #[builder(default, setter(into))]
469 lastname: Option<Cow<'a, str>>,
470 #[builder(default, setter(into))]
472 mail: Option<Cow<'a, str>>,
473 #[builder(default)]
475 auth_source_id: Option<u64>,
476 #[builder(default)]
478 mail_notification: Option<MailNotificationOptions>,
479 #[builder(default)]
481 must_change_passwd: Option<bool>,
482 #[builder(default)]
484 generate_password: Option<bool>,
485 #[builder(default)]
487 #[serde(skip_serializing)]
488 send_information: Option<bool>,
489 #[builder(default)]
491 admin: Option<bool>,
492}
493
494impl<'a> UpdateUser<'a> {
495 #[must_use]
497 pub fn builder() -> UpdateUserBuilder<'a> {
498 UpdateUserBuilder::default()
499 }
500}
501
502impl Endpoint for UpdateUser<'_> {
503 fn method(&self) -> Method {
504 Method::PUT
505 }
506
507 fn endpoint(&self) -> Cow<'static, str> {
508 format!("users/{}.json", self.id).into()
509 }
510
511 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
512 Ok(Some((
513 "application/json",
514 serde_json::to_vec(&UserWrapperWithSendInformation::<UpdateUser> {
515 user: (*self).to_owned(),
516 send_information: self.send_information,
517 })?,
518 )))
519 }
520}
521
522#[derive(Debug, Clone, Builder)]
524#[builder(setter(strip_option))]
525pub struct DeleteUser {
526 id: u64,
528}
529
530impl DeleteUser {
531 #[must_use]
533 pub fn builder() -> DeleteUserBuilder {
534 DeleteUserBuilder::default()
535 }
536}
537
538impl Endpoint for DeleteUser {
539 fn method(&self) -> Method {
540 Method::DELETE
541 }
542
543 fn endpoint(&self) -> Cow<'static, str> {
544 format!("users/{}.json", &self.id).into()
545 }
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
550pub struct UsersWrapper<T> {
551 pub users: Vec<T>,
553}
554
555#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
558pub struct UserWrapper<T> {
559 pub user: T,
561}
562
563#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
567pub struct UserWrapperWithSendInformation<T> {
568 pub user: T,
570 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub send_information: Option<bool>,
573}
574
575#[cfg(test)]
576pub(crate) mod test {
577 use crate::api::{
578 ResponsePage, groups::test::GROUP_LOCK, project_memberships::test::PROJECT_MEMBERSHIP_LOCK,
579 };
580
581 use super::*;
582 use pretty_assertions::assert_eq;
583 use std::error::Error;
584 use tokio::sync::RwLock;
585 use tracing_test::traced_test;
586
587 pub static USER_LOCK: RwLock<()> = RwLock::const_new(());
590
591 #[traced_test]
592 #[test]
593 fn test_list_users_first_page() -> Result<(), Box<dyn Error>> {
594 let _r_user = USER_LOCK.blocking_read();
595 dotenvy::dotenv()?;
596 let redmine = crate::api::Redmine::from_env(
597 reqwest::blocking::Client::builder()
598 .use_rustls_tls()
599 .build()?,
600 )?;
601 let endpoint = ListUsers::builder().build()?;
602 redmine.json_response_body_page::<_, User>(&endpoint, 0, 25)?;
603 Ok(())
604 }
605
606 #[traced_test]
607 #[test]
608 fn test_list_users_all_pages() -> Result<(), Box<dyn Error>> {
609 let _r_user = USER_LOCK.blocking_read();
610 dotenvy::dotenv()?;
611 let redmine = crate::api::Redmine::from_env(
612 reqwest::blocking::Client::builder()
613 .use_rustls_tls()
614 .build()?,
615 )?;
616 let endpoint = ListUsers::builder().build()?;
617 redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
618 Ok(())
619 }
620
621 #[traced_test]
622 #[test]
623 fn test_get_user() -> Result<(), Box<dyn Error>> {
624 let _r_user = USER_LOCK.blocking_read();
625 dotenvy::dotenv()?;
626 let redmine = crate::api::Redmine::from_env(
627 reqwest::blocking::Client::builder()
628 .use_rustls_tls()
629 .build()?,
630 )?;
631 let endpoint = GetUser::builder().id(1).build()?;
632 redmine.json_response_body::<_, UserWrapper<User>>(&endpoint)?;
633 Ok(())
634 }
635
636 #[function_name::named]
637 #[traced_test]
638 #[test]
639 fn test_create_user() -> Result<(), Box<dyn Error>> {
640 let _w_user = USER_LOCK.blocking_write();
641 let name = format!("unittest_{}", function_name!());
642 dotenvy::dotenv()?;
643 let redmine = crate::api::Redmine::from_env(
644 reqwest::blocking::Client::builder()
645 .use_rustls_tls()
646 .build()?,
647 )?;
648 let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
649 let users: Vec<User> = redmine.json_response_body_all_pages(&list_endpoint)?;
650 for user in users {
651 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
652 redmine.ignore_response_body::<_>(&delete_endpoint)?;
653 }
654 let create_endpoint = CreateUser::builder()
655 .login(name.clone())
656 .firstname("Unit")
657 .lastname("Test")
658 .mail(format!("unit-test_{name}@example.org"))
659 .build()?;
660 let UserWrapper { user } =
661 redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
662 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
663 redmine.ignore_response_body::<_>(&delete_endpoint)?;
664 Ok(())
665 }
666
667 #[function_name::named]
734 #[traced_test]
735 #[test]
736 fn test_update_user() -> Result<(), Box<dyn Error>> {
737 let _w_user = USER_LOCK.blocking_write();
738 let name = format!("unittest_{}", function_name!());
739 dotenvy::dotenv()?;
740 let redmine = crate::api::Redmine::from_env(
741 reqwest::blocking::Client::builder()
742 .use_rustls_tls()
743 .build()?,
744 )?;
745 let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
746 let users: Vec<User> = redmine.json_response_body_all_pages(&list_endpoint)?;
747 for user in users {
748 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
749 redmine.ignore_response_body::<_>(&delete_endpoint)?;
750 }
751 let create_endpoint = CreateUser::builder()
752 .login(name.clone())
753 .firstname("Unit")
754 .lastname("Test")
755 .mail(format!("unit-test_{name}@example.org"))
756 .build()?;
757 let UserWrapper { user } =
758 redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
759 let update_endpoint = super::UpdateUser::builder()
760 .id(user.id)
761 .login(format!("new_{name}"))
762 .build()?;
763 redmine.ignore_response_body::<_>(&update_endpoint)?;
764 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
765 redmine.ignore_response_body::<_>(&delete_endpoint)?;
766 Ok(())
767 }
768
769 #[traced_test]
774 #[test]
775 fn test_completeness_user_type_first_page() -> Result<(), Box<dyn Error>> {
776 let _r_user = USER_LOCK.blocking_read();
777 dotenvy::dotenv()?;
778 let redmine = crate::api::Redmine::from_env(
779 reqwest::blocking::Client::builder()
780 .use_rustls_tls()
781 .build()?,
782 )?;
783 let endpoint = ListUsers::builder().build()?;
784 let ResponsePage {
785 values,
786 total_count: _,
787 offset: _,
788 limit: _,
789 } = redmine.json_response_body_page::<_, serde_json::Value>(&endpoint, 0, 100)?;
790 for value in values {
791 let o: User = serde_json::from_value(value.clone())?;
792 let reserialized = serde_json::to_value(o)?;
793 assert_eq!(value, reserialized);
794 }
795 Ok(())
796 }
797
798 #[traced_test]
806 #[test]
807 fn test_completeness_user_type_all_pages_all_user_details() -> Result<(), Box<dyn Error>> {
808 let _r_user = USER_LOCK.blocking_read();
809 let _r_groups = GROUP_LOCK.blocking_read();
810 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.blocking_read();
811 dotenvy::dotenv()?;
812 let redmine = crate::api::Redmine::from_env(
813 reqwest::blocking::Client::builder()
814 .use_rustls_tls()
815 .build()?,
816 )?;
817 let endpoint = ListUsers::builder().build()?;
818 let users = redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
819 for user in users {
820 let get_endpoint = GetUser::builder()
821 .id(user.id)
822 .include(vec![UserInclude::Memberships, UserInclude::Groups])
823 .build()?;
824 let UserWrapper { user: value } =
825 redmine.json_response_body::<_, UserWrapper<serde_json::Value>>(&get_endpoint)?;
826 let o: User = serde_json::from_value(value.clone())?;
827 let reserialized = serde_json::to_value(o)?;
828 assert_eq!(value, reserialized);
829 }
830 Ok(())
831 }
832}