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/{id}.json").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)]
470pub(crate) mod test {
471    use crate::api::{
472        groups::test::GROUP_LOCK, project_memberships::test::PROJECT_MEMBERSHIP_LOCK, ResponsePage,
473    };
474
475    use super::*;
476    use pretty_assertions::assert_eq;
477    use std::error::Error;
478    use tokio::sync::RwLock;
479    use tracing_test::traced_test;
480
481    /// needed so we do not get 404s when listing while
482    /// creating/deleting or creating/updating/deleting
483    pub static USER_LOCK: RwLock<()> = RwLock::const_new(());
484
485    #[traced_test]
486    #[test]
487    fn test_list_users_first_page() -> Result<(), Box<dyn Error>> {
488        let _r_user = USER_LOCK.blocking_read();
489        dotenvy::dotenv()?;
490        let redmine = crate::api::Redmine::from_env(
491            reqwest::blocking::Client::builder()
492                .use_rustls_tls()
493                .build()?,
494        )?;
495        let endpoint = ListUsers::builder().build()?;
496        redmine.json_response_body_page::<_, User>(&endpoint, 0, 25)?;
497        Ok(())
498    }
499
500    #[traced_test]
501    #[test]
502    fn test_list_users_all_pages() -> Result<(), Box<dyn Error>> {
503        let _r_user = USER_LOCK.blocking_read();
504        dotenvy::dotenv()?;
505        let redmine = crate::api::Redmine::from_env(
506            reqwest::blocking::Client::builder()
507                .use_rustls_tls()
508                .build()?,
509        )?;
510        let endpoint = ListUsers::builder().build()?;
511        redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
512        Ok(())
513    }
514
515    #[traced_test]
516    #[test]
517    fn test_get_user() -> Result<(), Box<dyn Error>> {
518        let _r_user = USER_LOCK.blocking_read();
519        dotenvy::dotenv()?;
520        let redmine = crate::api::Redmine::from_env(
521            reqwest::blocking::Client::builder()
522                .use_rustls_tls()
523                .build()?,
524        )?;
525        let endpoint = GetUser::builder().id(1).build()?;
526        redmine.json_response_body::<_, UserWrapper<User>>(&endpoint)?;
527        Ok(())
528    }
529
530    #[function_name::named]
531    #[traced_test]
532    #[test]
533    fn test_create_user() -> Result<(), Box<dyn Error>> {
534        let _w_user = USER_LOCK.blocking_write();
535        let name = format!("unittest_{}", function_name!());
536        dotenvy::dotenv()?;
537        let redmine = crate::api::Redmine::from_env(
538            reqwest::blocking::Client::builder()
539                .use_rustls_tls()
540                .build()?,
541        )?;
542        let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
543        let users: Vec<User> = redmine.json_response_body_all_pages(&list_endpoint)?;
544        for user in users {
545            let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
546            redmine.ignore_response_body::<_>(&delete_endpoint)?;
547        }
548        let create_endpoint = CreateUser::builder()
549            .login(name.clone())
550            .firstname("Unit")
551            .lastname("Test")
552            .mail(format!("unit-test_{name}@example.org"))
553            .build()?;
554        let UserWrapper { user } =
555            redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
556        let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
557        redmine.ignore_response_body::<_>(&delete_endpoint)?;
558        Ok(())
559    }
560
561    // this test causes emails to be sent so we comment it out, mainly it was
562    // meant to check if the send_information attribute is inside or outside the
563    // user object in CreateUser (the docs in the wiki say outside and that really
564    // seems to be the case)
565    // #[function_name::named]
566    // #[traced_test]
567    // #[test]
568    // fn test_create_user_send_account_info() -> Result<(), Box<dyn Error>> {
569    //     let _w_user = USER_LOCK.blocking_write();
570    //     let name = format!("unittest_{}", function_name!());
571    //     dotenvy::dotenv()?;
572    //     let redmine = crate::api::Redmine::from_env()?;
573    //     let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
574    //     let UsersWrapper { users } =
575    //         redmine.json_response_body::<_, UsersWrapper<User>>(&list_endpoint)?;
576    //     for user in users {
577    //         let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
578    //         redmine.ignore_response_body::<_>(&delete_endpoint)?;
579    //     }
580    //     let create_endpoint = CreateUser::builder()
581    //         .login(name.clone())
582    //         .firstname("Unit")
583    //         .lastname("Test Send Account Info")
584    //         .mail(format!("{}@example.org", name)) // apparently there is a 60 character limit on the email in Redmine
585    //         .send_information(true)
586    //         .build()?;
587    //     let UserWrapper { user } =
588    //         redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
589    //     let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
590    //     redmine.ignore_response_body::<_>(&delete_endpoint)?;
591    //     Ok(())
592    // }
593
594    // this test causes emails to be sent so we comment it out, mainly it was
595    // meant to check if the admin attribute is inside or outside the user object
596    // in CreateUser (the docs on the wiki say outside but inside seems
597    // to be correct)
598    // #[function_name::named]
599    // #[traced_test]
600    // #[test]
601    // fn test_create_admin_user() -> Result<(), Box<dyn Error>> {
602    //     let _w_user = USER_LOCK.blocking_write();
603    //     let name = format!("unittest_{}", function_name!());
604    //     dotenvy::dotenv()?;
605    //     let redmine = crate::api::Redmine::from_env()?;
606    //     let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
607    //     let UsersWrapper { users } =
608    //         redmine.json_response_body::<_, UsersWrapper<User>>(&list_endpoint)?;
609    //     for user in users {
610    //         let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
611    //         redmine.ignore_response_body::<_>(&delete_endpoint)?;
612    //     }
613    //     let create_endpoint = CreateUser::builder()
614    //         .login(name.clone())
615    //         .firstname("Unit")
616    //         .lastname("Test Admin")
617    //         .mail(format!("unit-test_{}@example.org", name))
618    //         .admin(true)
619    //         .build()?;
620    //     let UserWrapper { user } =
621    //         redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
622    //     let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
623    //     redmine.ignore_response_body::<_>(&delete_endpoint)?;
624    //     Ok(())
625    // }
626
627    #[function_name::named]
628    #[traced_test]
629    #[test]
630    fn test_update_user() -> Result<(), Box<dyn Error>> {
631        let _w_user = USER_LOCK.blocking_write();
632        let name = format!("unittest_{}", function_name!());
633        dotenvy::dotenv()?;
634        let redmine = crate::api::Redmine::from_env(
635            reqwest::blocking::Client::builder()
636                .use_rustls_tls()
637                .build()?,
638        )?;
639        let list_endpoint = ListUsers::builder().name(name.clone()).build()?;
640        let users: Vec<User> = redmine.json_response_body_all_pages(&list_endpoint)?;
641        for user in users {
642            let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
643            redmine.ignore_response_body::<_>(&delete_endpoint)?;
644        }
645        let create_endpoint = CreateUser::builder()
646            .login(name.clone())
647            .firstname("Unit")
648            .lastname("Test")
649            .mail(format!("unit-test_{name}@example.org"))
650            .build()?;
651        let UserWrapper { user } =
652            redmine.json_response_body::<_, UserWrapper<User>>(&create_endpoint)?;
653        let update_endpoint = super::UpdateUser::builder()
654            .id(user.id)
655            .login(format!("new_{name}"))
656            .build()?;
657        redmine.ignore_response_body::<_>(&update_endpoint)?;
658        let delete_endpoint = DeleteUser::builder().id(user.id).build()?;
659        redmine.ignore_response_body::<_>(&delete_endpoint)?;
660        Ok(())
661    }
662
663    /// this tests if any of the results contain a field we are not deserializing
664    ///
665    /// this will only catch fields we missed if they are part of the response but
666    /// it is better than nothing
667    #[traced_test]
668    #[test]
669    fn test_completeness_user_type_first_page() -> Result<(), Box<dyn Error>> {
670        let _r_user = USER_LOCK.blocking_read();
671        dotenvy::dotenv()?;
672        let redmine = crate::api::Redmine::from_env(
673            reqwest::blocking::Client::builder()
674                .use_rustls_tls()
675                .build()?,
676        )?;
677        let endpoint = ListUsers::builder().build()?;
678        let ResponsePage {
679            values,
680            total_count: _,
681            offset: _,
682            limit: _,
683        } = redmine.json_response_body_page::<_, serde_json::Value>(&endpoint, 0, 100)?;
684        for value in values {
685            let o: User = serde_json::from_value(value.clone())?;
686            let reserialized = serde_json::to_value(o)?;
687            assert_eq!(value, reserialized);
688        }
689        Ok(())
690    }
691
692    /// this tests if any of the results contain a field we are not deserializing
693    ///
694    /// this will only catch fields we missed if they are part of the response but
695    /// it is better than nothing
696    ///
697    /// this version of the test will load all pages of users and the individual
698    /// users for each via GetUser
699    #[traced_test]
700    #[test]
701    fn test_completeness_user_type_all_pages_all_user_details() -> Result<(), Box<dyn Error>> {
702        let _r_user = USER_LOCK.blocking_read();
703        let _r_groups = GROUP_LOCK.blocking_read();
704        let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.blocking_read();
705        dotenvy::dotenv()?;
706        let redmine = crate::api::Redmine::from_env(
707            reqwest::blocking::Client::builder()
708                .use_rustls_tls()
709                .build()?,
710        )?;
711        let endpoint = ListUsers::builder().build()?;
712        let users = redmine.json_response_body_all_pages::<_, User>(&endpoint)?;
713        for user in users {
714            let get_endpoint = GetUser::builder()
715                .id(user.id)
716                .include(vec![UserInclude::Memberships, UserInclude::Groups])
717                .build()?;
718            let UserWrapper { user: value } =
719                redmine.json_response_body::<_, UserWrapper<serde_json::Value>>(&get_endpoint)?;
720            let o: User = serde_json::from_value(value.clone())?;
721            let reserialized = serde_json::to_value(o)?;
722            assert_eq!(value, reserialized);
723        }
724        Ok(())
725    }
726}