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, 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 {}
220
221impl GetUser {
222    /// Create a builder for the endpoint.
223    #[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/// Possible values for mail notification options for a user
249#[derive(Debug, Clone, Serialize)]
250#[serde(rename_all = "snake_case")]
251pub enum MailNotificationOptions {
252    /// Get notified by all events (visible to user)
253    All,
254    /// This allows to be notified only by selected projects, not sure if those can be selected via the API
255    Selected,
256    /// Only get notifications for events caused by the user's own actions
257    OnlyMyEvents,
258    /// Only get notifications for events in issues assigned to the user
259    OnlyAssigned,
260    /// Only get notifications for events in issues owned by the user
261    OnlyOwner,
262    /// Do not get any notifications
263    #[serde(rename = "none")]
264    NoMailNotifications,
265}
266
267/// The endpoint to create a Redmine user
268#[serde_with::skip_serializing_none]
269#[derive(Debug, Clone, Builder, Serialize)]
270#[builder(setter(strip_option))]
271pub struct CreateUser<'a> {
272    /// The login for the user
273    #[builder(setter(into))]
274    login: Cow<'a, str>,
275    /// The user's password
276    ///
277    /// It is recommended to use generate_password instead
278    #[builder(setter(into), default)]
279    password: Option<Cow<'a, str>>,
280    /// The user's firstname
281    #[builder(setter(into))]
282    firstname: Cow<'a, str>,
283    /// The user's lastname
284    #[builder(setter(into))]
285    lastname: Cow<'a, str>,
286    /// The users primary email address
287    #[builder(setter(into))]
288    mail: Cow<'a, str>,
289    /// allows setting users to be e.g. LDAP users
290    #[builder(default)]
291    auth_source_id: Option<u64>,
292    /// what kind of mail notifications should be sent to the user
293    #[builder(default)]
294    mail_notification: Option<MailNotificationOptions>,
295    /// if set the user must change their password after the next login
296    #[builder(default)]
297    must_change_passwd: Option<bool>,
298    /// generate a random password
299    #[builder(default)]
300    generate_password: Option<bool>,
301    /// Send account information to the user
302    #[builder(default)]
303    #[serde(skip_serializing)]
304    send_information: Option<bool>,
305    /// Make the user a Redmine administrator
306    #[builder(default)]
307    admin: Option<bool>,
308}
309
310impl ReturnsJsonResponse for CreateUser<'_> {}
311
312impl<'a> CreateUser<'a> {
313    /// Create a builder for the endpoint.
314    #[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/// The endpoint to update an existing Redmine user
341#[serde_with::skip_serializing_none]
342#[derive(Debug, Clone, Builder, Serialize)]
343#[builder(setter(strip_option))]
344pub struct UpdateUser<'a> {
345    /// The id of the user to update
346    #[serde(skip_serializing)]
347    id: u64,
348    /// The login for the user
349    #[builder(setter(into))]
350    login: Cow<'a, str>,
351    /// The user's password
352    ///
353    /// It is recommended to use generate_password instead
354    #[builder(setter(into), default)]
355    password: Option<Cow<'a, str>>,
356    /// The user's firstname
357    #[builder(default, setter(into))]
358    firstname: Option<Cow<'a, str>>,
359    /// The user's lastname
360    #[builder(default, setter(into))]
361    lastname: Option<Cow<'a, str>>,
362    /// The users primary email address
363    #[builder(default, setter(into))]
364    mail: Option<Cow<'a, str>>,
365    /// allows setting users to be e.g. LDAP users
366    #[builder(default)]
367    auth_source_id: Option<u64>,
368    /// what kind of mail notifications should be sent to the user
369    #[builder(default)]
370    mail_notification: Option<MailNotificationOptions>,
371    /// if set the user must change their password after the next login
372    #[builder(default)]
373    must_change_passwd: Option<bool>,
374    /// generate a random password
375    #[builder(default)]
376    generate_password: Option<bool>,
377    /// Send account information to the user
378    #[builder(default)]
379    #[serde(skip_serializing)]
380    send_information: Option<bool>,
381    /// Make the user a Redmine administrator
382    #[builder(default)]
383    admin: Option<bool>,
384}
385
386impl<'a> UpdateUser<'a> {
387    /// Create a builder for the endpoint.
388    #[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/// The endpoint to delete a Redmine user
415#[derive(Debug, Clone, Builder)]
416#[builder(setter(strip_option))]
417pub struct DeleteUser {
418    /// The id of the user to delete
419    id: u64,
420}
421
422impl DeleteUser {
423    /// Create a builder for the endpoint.
424    #[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/// helper struct for outer layers with a users field holding the inner data
441#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
442pub struct UsersWrapper<T> {
443    /// to parse JSON with users key
444    pub users: Vec<T>,
445}
446
447/// A lot of APIs in Redmine wrap their data in an extra layer, this is a
448/// helper struct for outer layers with a user field holding the inner data
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
450pub struct UserWrapper<T> {
451    /// to parse JSON with user key
452    pub user: T,
453}
454
455/// a special version of the UserWrapper to use with [CreateUser] and [UpdateUser]
456/// because Redmine puts the send_information flag outside the user object for
457/// some reason
458#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
459pub struct UserWrapperWithSendInformation<T> {
460    /// to parse JSON with user key
461    pub user: T,
462    /// send information flag in [CreateUser] and [UpdateUser]
463    #[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    /// needed so we do not get 404s when listing while
476    /// creating/deleting or creating/updating/deleting
477    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    // this test causes emails to be sent so we comment it out, mainly it was
552    // meant to check if the send_information attribute is inside or outside the
553    // user object in CreateUser (the docs in the wiki say outside and that really
554    // seems to be the case)
555    // #[function_name::named]
556    // #[traced_test]
557    // #[test]
558    // fn test_create_user_send_account_info() -> Result<(), Box<dyn Error>> {
559    //     let _w_user = USER_LOCK.write();
560    //     let name = format!("unittest_{}", function_name!());
561    //     dotenvy::dotenv()?;
562    //     let redmine = crate::api::Redmine::from_env()?;
563    //     let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
564    //     let UsersWrapper { users } =
565    //         redmine.json_response_body::<_, UsersWrapper<User>>(&list_endpoint)?;
566    //     for user in users {
567    //         let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
568    //         redmine.ignore_response_body::<_>(&delete_endpoint)?;
569    //     }
570    //     let create_endpoint = CreateUser::builder()
571    //         .login(name.clone())
572    //         .firstname("Unit")
573    //         .lastname("Test Send Account Info")
574    //         .mail(format!("{}@example.org", name)) // apparently there is a 60 character limit on the email in Redmine
575    //         .send_information(true)
576    //         .build()?;
577    //     let UserWrapper { user } =
578    //         redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
579    //     let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
580    //     redmine.ignore_response_body::<_>(&delete_endpoint)?;
581    //     Ok(())
582    // }
583
584    // this test causes emails to be sent so we comment it out, mainly it was
585    // meant to check if the admin attribute is inside or outside the user object
586    // in CreateUser (the docs on the wiki say outside but inside seems
587    // to be correct)
588    // #[function_name::named]
589    // #[traced_test]
590    // #[test]
591    // fn test_create_admin_user() -> Result<(), Box<dyn Error>> {
592    //     let _w_user = USER_LOCK.write();
593    //     let name = format!("unittest_{}", function_name!());
594    //     dotenvy::dotenv()?;
595    //     let redmine = crate::api::Redmine::from_env()?;
596    //     let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
597    //     let UsersWrapper { users } =
598    //         redmine.json_response_body::<_, UsersWrapper<User>>(&list_endpoint)?;
599    //     for user in users {
600    //         let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
601    //         redmine.ignore_response_body::<_>(&delete_endpoint)?;
602    //     }
603    //     let create_endpoint = CreateUser::builder()
604    //         .login(name.clone())
605    //         .firstname("Unit")
606    //         .lastname("Test Admin")
607    //         .mail(format!("unit-test_{}@example.org", name))
608    //         .admin(true)
609    //         .build()?;
610    //     let UserWrapper { user } =
611    //         redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
612    //     let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
613    //     redmine.ignore_response_body::<_>(&delete_endpoint)?;
614    //     Ok(())
615    // }
616
617    #[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    /// this tests if any of the results contain a field we are not deserializing
651    ///
652    /// this will only catch fields we missed if they are part of the response but
653    /// it is better than nothing
654    #[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    /// this tests if any of the results contain a field we are not deserializing
672    ///
673    /// this will only catch fields we missed if they are part of the response but
674    /// it is better than nothing
675    ///
676    /// this version of the test will load all pages of users and the individual
677    /// users for each via GetUser
678    #[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}