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, 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 {}
220
221impl GetUser {
222 #[must_use]
224 pub fn builder() -> GetUserBuilder {
225 GetUserBuilder::default()
226 }
227}
228
229impl Endpoint for GetUser {
230 fn method(&self) -> Method {
231 Method::GET
232 }
233
234 fn endpoint(&self) -> Cow<'static, str> {
235 match self.id {
236 Some(id) => format!("users/{}.json", id).into(),
237 None => "users/current.json".into(),
238 }
239 }
240
241 fn parameters(&self) -> QueryParams {
242 let mut params = QueryParams::default();
243 params.push_opt("include", self.include.as_ref());
244 params
245 }
246}
247
248#[derive(Debug, Clone, Serialize)]
250#[serde(rename_all = "snake_case")]
251pub enum MailNotificationOptions {
252 All,
254 Selected,
256 OnlyMyEvents,
258 OnlyAssigned,
260 OnlyOwner,
262 #[serde(rename = "none")]
264 NoMailNotifications,
265}
266
267#[serde_with::skip_serializing_none]
269#[derive(Debug, Clone, Builder, Serialize)]
270#[builder(setter(strip_option))]
271pub struct CreateUser<'a> {
272 #[builder(setter(into))]
274 login: Cow<'a, str>,
275 #[builder(setter(into), default)]
279 password: Option<Cow<'a, str>>,
280 #[builder(setter(into))]
282 firstname: Cow<'a, str>,
283 #[builder(setter(into))]
285 lastname: Cow<'a, str>,
286 #[builder(setter(into))]
288 mail: Cow<'a, str>,
289 #[builder(default)]
291 auth_source_id: Option<u64>,
292 #[builder(default)]
294 mail_notification: Option<MailNotificationOptions>,
295 #[builder(default)]
297 must_change_passwd: Option<bool>,
298 #[builder(default)]
300 generate_password: Option<bool>,
301 #[builder(default)]
303 #[serde(skip_serializing)]
304 send_information: Option<bool>,
305 #[builder(default)]
307 admin: Option<bool>,
308}
309
310impl ReturnsJsonResponse for CreateUser<'_> {}
311
312impl<'a> CreateUser<'a> {
313 #[must_use]
315 pub fn builder() -> CreateUserBuilder<'a> {
316 CreateUserBuilder::default()
317 }
318}
319
320impl Endpoint for CreateUser<'_> {
321 fn method(&self) -> Method {
322 Method::POST
323 }
324
325 fn endpoint(&self) -> Cow<'static, str> {
326 "users.json".into()
327 }
328
329 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
330 Ok(Some((
331 "application/json",
332 serde_json::to_vec(&UserWrapperWithSendInformation::<CreateUser> {
333 user: (*self).to_owned(),
334 send_information: self.send_information,
335 })?,
336 )))
337 }
338}
339
340#[serde_with::skip_serializing_none]
342#[derive(Debug, Clone, Builder, Serialize)]
343#[builder(setter(strip_option))]
344pub struct UpdateUser<'a> {
345 #[serde(skip_serializing)]
347 id: u64,
348 #[builder(setter(into))]
350 login: Cow<'a, str>,
351 #[builder(setter(into), default)]
355 password: Option<Cow<'a, str>>,
356 #[builder(default, setter(into))]
358 firstname: Option<Cow<'a, str>>,
359 #[builder(default, setter(into))]
361 lastname: Option<Cow<'a, str>>,
362 #[builder(default, setter(into))]
364 mail: Option<Cow<'a, str>>,
365 #[builder(default)]
367 auth_source_id: Option<u64>,
368 #[builder(default)]
370 mail_notification: Option<MailNotificationOptions>,
371 #[builder(default)]
373 must_change_passwd: Option<bool>,
374 #[builder(default)]
376 generate_password: Option<bool>,
377 #[builder(default)]
379 #[serde(skip_serializing)]
380 send_information: Option<bool>,
381 #[builder(default)]
383 admin: Option<bool>,
384}
385
386impl<'a> UpdateUser<'a> {
387 #[must_use]
389 pub fn builder() -> UpdateUserBuilder<'a> {
390 UpdateUserBuilder::default()
391 }
392}
393
394impl Endpoint for UpdateUser<'_> {
395 fn method(&self) -> Method {
396 Method::PUT
397 }
398
399 fn endpoint(&self) -> Cow<'static, str> {
400 format!("users/{}.json", self.id).into()
401 }
402
403 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
404 Ok(Some((
405 "application/json",
406 serde_json::to_vec(&UserWrapperWithSendInformation::<UpdateUser> {
407 user: (*self).to_owned(),
408 send_information: self.send_information,
409 })?,
410 )))
411 }
412}
413
414#[derive(Debug, Clone, Builder)]
416#[builder(setter(strip_option))]
417pub struct DeleteUser {
418 id: u64,
420}
421
422impl DeleteUser {
423 #[must_use]
425 pub fn builder() -> DeleteUserBuilder {
426 DeleteUserBuilder::default()
427 }
428}
429
430impl Endpoint for DeleteUser {
431 fn method(&self) -> Method {
432 Method::DELETE
433 }
434
435 fn endpoint(&self) -> Cow<'static, str> {
436 format!("users/{}.json", &self.id).into()
437 }
438}
439
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
442pub struct UsersWrapper<T> {
443 pub users: Vec<T>,
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
450pub struct UserWrapper<T> {
451 pub user: T,
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
459pub struct UserWrapperWithSendInformation<T> {
460 pub user: T,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub send_information: Option<bool>,
465}
466
467#[cfg(test)]
468mod test {
469 use super::*;
470 use pretty_assertions::assert_eq;
471 use std::error::Error;
472 use tokio::sync::RwLock;
473 use tracing_test::traced_test;
474
475 static USER_LOCK: RwLock<()> = RwLock::const_new(());
478
479 #[traced_test]
480 #[test]
481 fn test_list_users_no_pagination() -> Result<(), Box<dyn Error>> {
482 let _r_user = USER_LOCK.read();
483 dotenvy::dotenv()?;
484 let redmine = crate::api::Redmine::from_env()?;
485 let endpoint = ListUsers::builder().build()?;
486 redmine.json_response_body::<_, UsersWrapper<User>>(&endpoint)?;
487 Ok(())
488 }
489
490 #[traced_test]
491 #[test]
492 fn test_list_users_first_page() -> Result<(), Box<dyn Error>> {
493 let _r_user = USER_LOCK.read();
494 dotenvy::dotenv()?;
495 let redmine = crate::api::Redmine::from_env()?;
496 let endpoint = ListUsers::builder().build()?;
497 redmine.json_response_body_page::<_, User>(&endpoint, 0, 25)?;
498 Ok(())
499 }
500
501 #[traced_test]
502 #[test]
503 fn test_list_users_all_pages() -> Result<(), Box<dyn Error>> {
504 let _r_user = USER_LOCK.read();
505 dotenvy::dotenv()?;
506 let redmine = crate::api::Redmine::from_env()?;
507 let endpoint = ListUsers::builder().build()?;
508 redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
509 Ok(())
510 }
511
512 #[traced_test]
513 #[test]
514 fn test_get_user() -> Result<(), Box<dyn Error>> {
515 let _r_user = USER_LOCK.read();
516 dotenvy::dotenv()?;
517 let redmine = crate::api::Redmine::from_env()?;
518 let endpoint = GetUser::builder().id(1).build()?;
519 redmine.json_response_body::<_, UserWrapper<User>>(&endpoint)?;
520 Ok(())
521 }
522
523 #[function_name::named]
524 #[traced_test]
525 #[test]
526 fn test_create_user() -> Result<(), Box<dyn Error>> {
527 let _w_user = USER_LOCK.write();
528 let name = format!("unittest_{}", function_name!());
529 dotenvy::dotenv()?;
530 let redmine = crate::api::Redmine::from_env()?;
531 let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
532 let UsersWrapper { users } =
533 redmine.json_response_body::<_, UsersWrapper<User>>(&list_endpoint)?;
534 for user in users {
535 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
536 redmine.ignore_response_body::<_>(&delete_endpoint)?;
537 }
538 let create_endpoint = CreateUser::builder()
539 .login(name.clone())
540 .firstname("Unit")
541 .lastname("Test")
542 .mail(format!("unit-test_{}@example.org", name))
543 .build()?;
544 let UserWrapper { user } =
545 redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
546 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
547 redmine.ignore_response_body::<_>(&delete_endpoint)?;
548 Ok(())
549 }
550
551 #[function_name::named]
618 #[traced_test]
619 #[test]
620 fn test_update_user() -> Result<(), Box<dyn Error>> {
621 let _w_user = USER_LOCK.write();
622 let name = format!("unittest_{}", function_name!());
623 dotenvy::dotenv()?;
624 let redmine = crate::api::Redmine::from_env()?;
625 let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
626 let UsersWrapper { users } =
627 redmine.json_response_body::<_, UsersWrapper<User>>(&list_endpoint)?;
628 for user in users {
629 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
630 redmine.ignore_response_body::<_>(&delete_endpoint)?;
631 }
632 let create_endpoint = CreateUser::builder()
633 .login(name.clone())
634 .firstname("Unit")
635 .lastname("Test")
636 .mail(format!("unit-test_{}@example.org", name))
637 .build()?;
638 let UserWrapper { user } =
639 redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
640 let update_endpoint = super::UpdateUser::builder()
641 .id(user.id)
642 .login(format!("new_{}", name))
643 .build()?;
644 redmine.ignore_response_body::<_>(&update_endpoint)?;
645 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
646 redmine.ignore_response_body::<_>(&delete_endpoint)?;
647 Ok(())
648 }
649
650 #[traced_test]
655 #[test]
656 fn test_completeness_user_type() -> Result<(), Box<dyn Error>> {
657 let _r_user = USER_LOCK.read();
658 dotenvy::dotenv()?;
659 let redmine = crate::api::Redmine::from_env()?;
660 let endpoint = ListUsers::builder().build()?;
661 let UsersWrapper { users: values } =
662 redmine.json_response_body::<_, UsersWrapper<serde_json::Value>>(&endpoint)?;
663 for value in values {
664 let o: User = serde_json::from_value(value.clone())?;
665 let reserialized = serde_json::to_value(o)?;
666 assert_eq!(value, reserialized);
667 }
668 Ok(())
669 }
670
671 #[traced_test]
679 #[test]
680 fn test_completeness_user_type_all_pages_all_user_details() -> Result<(), Box<dyn Error>> {
681 let _r_user = USER_LOCK.read();
682 dotenvy::dotenv()?;
683 let redmine = crate::api::Redmine::from_env()?;
684 let endpoint = ListUsers::builder().build()?;
685 let users = redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
686 for user in users {
687 let get_endpoint = GetUser::builder()
688 .id(user.id)
689 .include(vec![UserInclude::Memberships, UserInclude::Groups])
690 .build()?;
691 let UserWrapper { user: value } =
692 redmine.json_response_body::<_, UserWrapper<serde_json::Value>>(&get_endpoint)?;
693 let o: User = serde_json::from_value(value.clone())?;
694 let reserialized = serde_json::to_value(o)?;
695 assert_eq!(value, reserialized);
696 }
697 Ok(())
698 }
699}