redmine_api/api/
users.rs

1//! Users Rest API Endpoint definitions
2//!
3//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_Users)
4//!
5//! - [x] all users endpoint
6//!   - [x] status filter
7//!   - [x] name filter
8//!   - [x] group_id filter
9//! - [x] specific user endpoint
10//!   - [x] by user id
11//!   - [x] current
12//! - [x] create user endpoint
13//! - [x] update user endpoint
14//! - [x] delete user endpoint
15
16use 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/// a minimal type for Redmine users used in
27/// other Redmine objects (e.g. issue author)
28#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29pub struct UserEssentials {
30    /// numeric id
31    pub id: u64,
32    /// display name
33    pub name: String,
34}
35
36/// a type for user to use as an API return type
37///
38/// alternatively you can use your own type limited to the fields you need
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
40pub struct User {
41    /// numeric id
42    pub id: u64,
43    /// user status (seemingly numeric here, unlike filters)
44    ///
45    /// TODO: turn this into the Enum UserStatus?
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub status: Option<u64>,
48    /// login name
49    pub login: String,
50    /// is this user an admin
51    pub admin: bool,
52    /// user's firstname
53    pub firstname: String,
54    /// user's lastname
55    pub lastname: String,
56    /// primary email of the user
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub mail: Option<String>,
59    /// the user's API key
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub api_key: Option<String>,
62    /// user's 2FA scheme
63    #[serde(default)]
64    pub twofa_scheme: Option<String>,
65    /// allows setting users to be e.g. LDAP users
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    auth_source_id: Option<u64>,
68    /// The time when this user was created
69    #[serde(
70        serialize_with = "crate::api::serialize_rfc3339",
71        deserialize_with = "crate::api::deserialize_rfc3339"
72    )]
73    pub created_on: time::OffsetDateTime,
74    /// The time when this user was last updated
75    #[serde(
76        serialize_with = "crate::api::serialize_rfc3339",
77        deserialize_with = "crate::api::deserialize_rfc3339"
78    )]
79    pub updated_on: time::OffsetDateTime,
80    /// The time when this user's password was last changed
81    #[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    /// the time when this user last logged in
87    #[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    /// custom fields with values
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
95    /// groups (only if include is specified)
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub groups: Option<Vec<GroupEssentials>>,
98    /// memberships (only if include is specified)
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub memberships: Option<Vec<UserProjectMembership>>,
101}
102
103/// The user status values for filtering
104#[derive(Debug, Clone)]
105pub enum UserStatus {
106    /// User can login and use their account (default)
107    Active,
108    /// User has registered but not yet confirmed their email address or was not yet activated by an administrator. User can not login
109    Registered,
110    /// User was once active and is now locked, User can not login
111    Locked,
112    /// Specify this to get users with any status
113    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/// The endpoint for all users
136#[derive(Debug, Clone, Builder)]
137#[builder(setter(strip_option))]
138pub struct ListUsers<'a> {
139    /// Filter by user status
140    #[builder(default)]
141    /// The status of the users (locked, registered but not confirmed yet,...)
142    status: Option<UserStatus>,
143    #[builder(default)]
144    /// Filter by name, this matches login, firstname, lastname and if it contains a space also firstname and lastname
145    #[builder(setter(into))]
146    name: Option<Cow<'a, str>>,
147    /// Users need to be members of this group
148    #[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    /// Create a builder for the endpoint.
161    #[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/// The types of associated data which can be fetched along with a user
186#[derive(Debug, Clone)]
187pub enum UserInclude {
188    /// The project memberships of this user
189    Memberships,
190    /// The groups where this user is a member
191    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/// The endpoint for a specific user
208#[derive(Debug, Clone, Builder)]
209#[builder(setter(strip_option))]
210pub struct GetUser {
211    /// User id to fetch, if not specified will fetch the current user
212    #[builder(default)]
213    id: Option<u64>,
214    /// Include associated data
215    #[builder(default)]
216    include: Option<Vec<UserInclude>>,
217}
218
219impl ReturnsJsonResponse for GetUser {}
220impl NoPagination for GetUser {}
221
222impl GetUser {
223    /// Create a builder for the endpoint.
224    #[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/// Possible values for mail notification options for a user
250#[derive(Debug, Clone, Serialize)]
251#[serde(rename_all = "snake_case")]
252pub enum MailNotificationOptions {
253    /// Get notified by all events (visible to user)
254    All,
255    /// This allows to be notified only by selected projects, not sure if those can be selected via the API
256    Selected,
257    /// Only get notifications for events caused by the user's own actions
258    OnlyMyEvents,
259    /// Only get notifications for events in issues assigned to the user
260    OnlyAssigned,
261    /// Only get notifications for events in issues owned by the user
262    OnlyOwner,
263    /// Do not get any notifications
264    #[serde(rename = "none")]
265    NoMailNotifications,
266}
267
268/// The endpoint to create a Redmine user
269#[serde_with::skip_serializing_none]
270#[derive(Debug, Clone, Builder, Serialize)]
271#[builder(setter(strip_option))]
272pub struct CreateUser<'a> {
273    /// The login for the user
274    #[builder(setter(into))]
275    login: Cow<'a, str>,
276    /// The user's password
277    ///
278    /// It is recommended to use generate_password instead
279    #[builder(setter(into), default)]
280    password: Option<Cow<'a, str>>,
281    /// The user's firstname
282    #[builder(setter(into))]
283    firstname: Cow<'a, str>,
284    /// The user's lastname
285    #[builder(setter(into))]
286    lastname: Cow<'a, str>,
287    /// The users primary email address
288    #[builder(setter(into))]
289    mail: Cow<'a, str>,
290    /// allows setting users to be e.g. LDAP users
291    #[builder(default)]
292    auth_source_id: Option<u64>,
293    /// what kind of mail notifications should be sent to the user
294    #[builder(default)]
295    mail_notification: Option<MailNotificationOptions>,
296    /// if set the user must change their password after the next login
297    #[builder(default)]
298    must_change_passwd: Option<bool>,
299    /// generate a random password
300    #[builder(default)]
301    generate_password: Option<bool>,
302    /// Send account information to the user
303    #[builder(default)]
304    #[serde(skip_serializing)]
305    send_information: Option<bool>,
306    /// Make the user a Redmine administrator
307    #[builder(default)]
308    admin: Option<bool>,
309}
310
311impl ReturnsJsonResponse for CreateUser<'_> {}
312impl NoPagination for CreateUser<'_> {}
313
314impl<'a> CreateUser<'a> {
315    /// Create a builder for the endpoint.
316    #[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/// The endpoint to update an existing Redmine user
343#[serde_with::skip_serializing_none]
344#[derive(Debug, Clone, Builder, Serialize)]
345#[builder(setter(strip_option))]
346pub struct UpdateUser<'a> {
347    /// The id of the user to update
348    #[serde(skip_serializing)]
349    id: u64,
350    /// The login for the user
351    #[builder(setter(into))]
352    login: Cow<'a, str>,
353    /// The user's password
354    ///
355    /// It is recommended to use generate_password instead
356    #[builder(setter(into), default)]
357    password: Option<Cow<'a, str>>,
358    /// The user's firstname
359    #[builder(default, setter(into))]
360    firstname: Option<Cow<'a, str>>,
361    /// The user's lastname
362    #[builder(default, setter(into))]
363    lastname: Option<Cow<'a, str>>,
364    /// The users primary email address
365    #[builder(default, setter(into))]
366    mail: Option<Cow<'a, str>>,
367    /// allows setting users to be e.g. LDAP users
368    #[builder(default)]
369    auth_source_id: Option<u64>,
370    /// what kind of mail notifications should be sent to the user
371    #[builder(default)]
372    mail_notification: Option<MailNotificationOptions>,
373    /// if set the user must change their password after the next login
374    #[builder(default)]
375    must_change_passwd: Option<bool>,
376    /// generate a random password
377    #[builder(default)]
378    generate_password: Option<bool>,
379    /// Send account information to the user
380    #[builder(default)]
381    #[serde(skip_serializing)]
382    send_information: Option<bool>,
383    /// Make the user a Redmine administrator
384    #[builder(default)]
385    admin: Option<bool>,
386}
387
388impl<'a> UpdateUser<'a> {
389    /// Create a builder for the endpoint.
390    #[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/// The endpoint to delete a Redmine user
417#[derive(Debug, Clone, Builder)]
418#[builder(setter(strip_option))]
419pub struct DeleteUser {
420    /// The id of the user to delete
421    id: u64,
422}
423
424impl DeleteUser {
425    /// Create a builder for the endpoint.
426    #[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/// helper struct for outer layers with a users field holding the inner data
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
444pub struct UsersWrapper<T> {
445    /// to parse JSON with users key
446    pub users: Vec<T>,
447}
448
449/// A lot of APIs in Redmine wrap their data in an extra layer, this is a
450/// helper struct for outer layers with a user field holding the inner data
451#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
452pub struct UserWrapper<T> {
453    /// to parse JSON with user key
454    pub user: T,
455}
456
457/// a special version of the UserWrapper to use with [CreateUser] and [UpdateUser]
458/// because Redmine puts the send_information flag outside the user object for
459/// some reason
460#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
461pub struct UserWrapperWithSendInformation<T> {
462    /// to parse JSON with user key
463    pub user: T,
464    /// send information flag in [CreateUser] and [UpdateUser]
465    #[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    /// needed so we do not get 404s when listing while
480    /// creating/deleting or creating/updating/deleting
481    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    // this test causes emails to be sent so we comment it out, mainly it was
560    // meant to check if the send_information attribute is inside or outside the
561    // user object in CreateUser (the docs in the wiki say outside and that really
562    // seems to be the case)
563    // #[function_name::named]
564    // #[traced_test]
565    // #[test]
566    // fn test_create_user_send_account_info() -> Result<(), Box<dyn Error>> {
567    //     let _w_user = USER_LOCK.write();
568    //     let name = format!("unittest_{}", function_name!());
569    //     dotenvy::dotenv()?;
570    //     let redmine = crate::api::Redmine::from_env()?;
571    //     let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
572    //     let UsersWrapper { users } =
573    //         redmine.json_response_body::<_, UsersWrapper<User>>(&list_endpoint)?;
574    //     for user in users {
575    //         let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
576    //         redmine.ignore_response_body::<_>(&delete_endpoint)?;
577    //     }
578    //     let create_endpoint = CreateUser::builder()
579    //         .login(name.clone())
580    //         .firstname("Unit")
581    //         .lastname("Test Send Account Info")
582    //         .mail(format!("{}@example.org", name)) // apparently there is a 60 character limit on the email in Redmine
583    //         .send_information(true)
584    //         .build()?;
585    //     let UserWrapper { user } =
586    //         redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
587    //     let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
588    //     redmine.ignore_response_body::<_>(&delete_endpoint)?;
589    //     Ok(())
590    // }
591
592    // this test causes emails to be sent so we comment it out, mainly it was
593    // meant to check if the admin attribute is inside or outside the user object
594    // in CreateUser (the docs on the wiki say outside but inside seems
595    // to be correct)
596    // #[function_name::named]
597    // #[traced_test]
598    // #[test]
599    // fn test_create_admin_user() -> Result<(), Box<dyn Error>> {
600    //     let _w_user = USER_LOCK.write();
601    //     let name = format!("unittest_{}", function_name!());
602    //     dotenvy::dotenv()?;
603    //     let redmine = crate::api::Redmine::from_env()?;
604    //     let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
605    //     let UsersWrapper { users } =
606    //         redmine.json_response_body::<_, UsersWrapper<User>>(&list_endpoint)?;
607    //     for user in users {
608    //         let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
609    //         redmine.ignore_response_body::<_>(&delete_endpoint)?;
610    //     }
611    //     let create_endpoint = CreateUser::builder()
612    //         .login(name.clone())
613    //         .firstname("Unit")
614    //         .lastname("Test Admin")
615    //         .mail(format!("unit-test_{}@example.org", name))
616    //         .admin(true)
617    //         .build()?;
618    //     let UserWrapper { user } =
619    //         redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
620    //     let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
621    //     redmine.ignore_response_body::<_>(&delete_endpoint)?;
622    //     Ok(())
623    // }
624
625    #[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    /// this tests if any of the results contain a field we are not deserializing
662    ///
663    /// this will only catch fields we missed if they are part of the response but
664    /// it is better than nothing
665    #[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    /// this tests if any of the results contain a field we are not deserializing
691    ///
692    /// this will only catch fields we missed if they are part of the response but
693    /// it is better than nothing
694    ///
695    /// this version of the test will load all pages of users and the individual
696    /// users for each via GetUser
697    #[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}