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 reqwest::blocking::Client::builder()
490 .use_rustls_tls()
491 .build()?,
492 )?;
493 let endpoint = ListUsers::builder().build()?;
494 redmine.json_response_body_page::<_, User>(&endpoint, 0, 25)?;
495 Ok(())
496 }
497
498 #[traced_test]
499 #[test]
500 fn test_list_users_all_pages() -> Result<(), Box<dyn Error>> {
501 let _r_user = USER_LOCK.read();
502 dotenvy::dotenv()?;
503 let redmine = crate::api::Redmine::from_env(
504 reqwest::blocking::Client::builder()
505 .use_rustls_tls()
506 .build()?,
507 )?;
508 let endpoint = ListUsers::builder().build()?;
509 redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
510 Ok(())
511 }
512
513 #[traced_test]
514 #[test]
515 fn test_get_user() -> Result<(), Box<dyn Error>> {
516 let _r_user = USER_LOCK.read();
517 dotenvy::dotenv()?;
518 let redmine = crate::api::Redmine::from_env(
519 reqwest::blocking::Client::builder()
520 .use_rustls_tls()
521 .build()?,
522 )?;
523 let endpoint = GetUser::builder().id(1).build()?;
524 redmine.json_response_body::<_, UserWrapper<User>>(&endpoint)?;
525 Ok(())
526 }
527
528 #[function_name::named]
529 #[traced_test]
530 #[test]
531 fn test_create_user() -> Result<(), Box<dyn Error>> {
532 let _w_user = USER_LOCK.write();
533 let name = format!("unittest_{}", function_name!());
534 dotenvy::dotenv()?;
535 let redmine = crate::api::Redmine::from_env(
536 reqwest::blocking::Client::builder()
537 .use_rustls_tls()
538 .build()?,
539 )?;
540 let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
541 let users: Vec<User> = redmine.json_response_body_all_pages(&list_endpoint)?;
542 for user in users {
543 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
544 redmine.ignore_response_body::<_>(&delete_endpoint)?;
545 }
546 let create_endpoint = CreateUser::builder()
547 .login(name.clone())
548 .firstname("Unit")
549 .lastname("Test")
550 .mail(format!("unit-test_{}@example.org", name))
551 .build()?;
552 let UserWrapper { user } =
553 redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
554 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
555 redmine.ignore_response_body::<_>(&delete_endpoint)?;
556 Ok(())
557 }
558
559 #[function_name::named]
626 #[traced_test]
627 #[test]
628 fn test_update_user() -> Result<(), Box<dyn Error>> {
629 let _w_user = USER_LOCK.write();
630 let name = format!("unittest_{}", function_name!());
631 dotenvy::dotenv()?;
632 let redmine = crate::api::Redmine::from_env(
633 reqwest::blocking::Client::builder()
634 .use_rustls_tls()
635 .build()?,
636 )?;
637 let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
638 let users: Vec<User> = redmine.json_response_body_all_pages(&list_endpoint)?;
639 for user in users {
640 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
641 redmine.ignore_response_body::<_>(&delete_endpoint)?;
642 }
643 let create_endpoint = CreateUser::builder()
644 .login(name.clone())
645 .firstname("Unit")
646 .lastname("Test")
647 .mail(format!("unit-test_{}@example.org", name))
648 .build()?;
649 let UserWrapper { user } =
650 redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
651 let update_endpoint = super::UpdateUser::builder()
652 .id(user.id)
653 .login(format!("new_{}", name))
654 .build()?;
655 redmine.ignore_response_body::<_>(&update_endpoint)?;
656 let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
657 redmine.ignore_response_body::<_>(&delete_endpoint)?;
658 Ok(())
659 }
660
661 #[traced_test]
666 #[test]
667 fn test_completeness_user_type_first_page() -> Result<(), Box<dyn Error>> {
668 let _r_user = USER_LOCK.read();
669 dotenvy::dotenv()?;
670 let redmine = crate::api::Redmine::from_env(
671 reqwest::blocking::Client::builder()
672 .use_rustls_tls()
673 .build()?,
674 )?;
675 let endpoint = ListUsers::builder().build()?;
676 let ResponsePage {
677 values,
678 total_count: _,
679 offset: _,
680 limit: _,
681 } = redmine.json_response_body_page::<_, serde_json::Value>(&endpoint, 0, 100)?;
682 for value in values {
683 let o: User = serde_json::from_value(value.clone())?;
684 let reserialized = serde_json::to_value(o)?;
685 assert_eq!(value, reserialized);
686 }
687 Ok(())
688 }
689
690 #[traced_test]
698 #[test]
699 fn test_completeness_user_type_all_pages_all_user_details() -> Result<(), Box<dyn Error>> {
700 let _r_user = USER_LOCK.read();
701 dotenvy::dotenv()?;
702 let redmine = crate::api::Redmine::from_env(
703 reqwest::blocking::Client::builder()
704 .use_rustls_tls()
705 .build()?,
706 )?;
707 let endpoint = ListUsers::builder().build()?;
708 let users = redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
709 for user in users {
710 let get_endpoint = GetUser::builder()
711 .id(user.id)
712 .include(vec![UserInclude::Memberships, UserInclude::Groups])
713 .build()?;
714 let UserWrapper { user: value } =
715 redmine.json_response_body::<_, UserWrapper<serde_json::Value>>(&get_endpoint)?;
716 let o: User = serde_json::from_value(value.clone())?;
717 let reserialized = serde_json::to_value(o)?;
718 assert_eq!(value, reserialized);
719 }
720 Ok(())
721 }
722}