redmine_api/api/
issues.rs

1//! Issues Rest API Endpoint definitions
2//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_Issues)
3//!
4//! [Redmine Documentation Journals](https://www.redmine.org/projects/redmine/wiki/Rest_IssueJournals)
5//! (Journals in Redmine terminology are notes/comments and change histories for an issue)
6//!
7//! - [ ] all issues endpoint
8//!   - [x] sort
9//!     - [ ] limit sort to the existing columns only instead of a string value
10//!   - [x] query_id parameter
11//!   - [x] pagination
12//!   - [x] issue_id filter
13//!     - [x] issue id (multiple are possible, comma separated)
14//!   - [x] project_id filter
15//!     - [x] project id (multiple are possible, comma separated)
16//!   - [x] subproject_id filter
17//!     - [x] !* filter to only get parent project issues
18//!   - [x] tracker_id filter
19//!     - [x] tracker id (multiple are possible, comma separated)
20//!   - [x] status_id filter
21//!     - [x] open (default)
22//!     - [x] closed
23//!     - [x] for both
24//!     - [x] status id (multiple are possible, comma separated)
25//!   - [x] category_id filter
26//!     - [x] category id (multiple are possible, comma separated)
27//!   - [x] priority_id filter
28//!     - [x] priority id (multiple are possible, comma separated)
29//!   - [x] author_id filter
30//!     - [x] any
31//!     - [x] me
32//!     - [x] !me
33//!     - [x] user/group id (multiple are possible, comma separated)
34//!     - [x] negation of list
35//!   - [x] assigned_to_id filter
36//!     - [x] any
37//!     - [x] me
38//!     - [x] !me
39//!     - [x] user/group id (multiple are possible, comma separated)
40//!     - [x] negation of list
41//!     - [x] none (!*)
42//!   - [x] fixed_version_id filter (Target version, API uses old name)
43//!     - [x] version id (multiple are possible, comma separated)
44//!   - [x] is_private filter
45//!   - [x] parent_id filter
46//!     - [x] issue id (multiple are possible, comma separated)
47//!   - [x] custom field filter
48//!     - [x] exact match
49//!     - [ ] substring match
50//!     - [ ] what about multiple value custom fields?
51//!   - [x] subject filter
52//!     - [x] exact match
53//!     - [x] substring match
54//!   - [x] description filter
55//!     - [x] exact match
56//!     - [x] substring match
57//!   - [x] done_ratio filter
58//!     - [x] exact match
59//!     - [x] less than, greater than ?
60//!     - [x] range?
61//!   - [x] estimated_hours filter
62//!     - [x] exact match
63//!     - [x] less than, greater than ?
64//!     - [x] range?
65//!   - [x] created_on filter
66//!     - [x] exact match
67//!     - [x] less than, greater than
68//!     - [x] date range
69//!   - [x] updated_on filter
70//!     - [x] exact match
71//!     - [x] less than, greater than
72//!     - [x] date range
73//!   - [x] start_date filter
74//!     - [x] exact match
75//!     - [x] less than, greater than
76//!     - [x] date range
77//!   - [x] due_date filter
78//!     - [x] exact match
79//!     - [x] less than, greater than
80//!     - [x] date range
81//! - [x] specific issue endpoint
82//! - [x] create issue endpoint
83//!   - [ ] attachments
84//! - [x] update issue endpoint
85//!   - [ ] attachments
86//! - [x] delete issue endpoint
87//! - [x] add watcher endpoint
88//! - [x] remove watcher endpoint
89//! - [ ] fields for issue changesets
90//!
91use derive_builder::Builder;
92use reqwest::Method;
93use std::borrow::Cow;
94
95use crate::api::attachments::Attachment;
96use crate::api::custom_fields::{CustomField, CustomFieldEssentialsWithValue};
97use crate::api::enumerations::IssuePriorityEssentials;
98use crate::api::groups::{Group, GroupEssentials};
99use crate::api::issue_categories::IssueCategoryEssentials;
100use crate::api::issue_relations::IssueRelation;
101use crate::api::issue_statuses::IssueStatusEssentials;
102use crate::api::projects::ProjectEssentials;
103use crate::api::projects::ProjectStatusFilter;
104use crate::api::trackers::TrackerEssentials;
105use crate::api::users::UserEssentials;
106use crate::api::versions::VersionEssentials;
107use crate::api::{
108    CustomFieldFilter, DateFilter, DateTimeFilterPast, Endpoint, FloatFilter, IntegerFilter,
109    NoPagination, Pageable, QueryParams, ReturnsJsonResponse, StringFieldFilter,
110};
111use serde::Serialize;
112
113/// a minimal type for Redmine users or groups used in lists of assignees included in
114/// other Redmine objects
115#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
116pub struct AssigneeEssentials {
117    /// numeric id
118    pub id: u64,
119    /// display name
120    pub name: String,
121}
122
123impl From<UserEssentials> for AssigneeEssentials {
124    fn from(v: UserEssentials) -> Self {
125        AssigneeEssentials {
126            id: v.id,
127            name: v.name,
128        }
129    }
130}
131
132impl From<&UserEssentials> for AssigneeEssentials {
133    fn from(v: &UserEssentials) -> Self {
134        AssigneeEssentials {
135            id: v.id,
136            name: v.name.to_owned(),
137        }
138    }
139}
140
141impl From<GroupEssentials> for AssigneeEssentials {
142    fn from(v: GroupEssentials) -> Self {
143        AssigneeEssentials {
144            id: v.id,
145            name: v.name,
146        }
147    }
148}
149
150impl From<&GroupEssentials> for AssigneeEssentials {
151    fn from(v: &GroupEssentials) -> Self {
152        AssigneeEssentials {
153            id: v.id,
154            name: v.name.to_owned(),
155        }
156    }
157}
158
159impl From<Group> for AssigneeEssentials {
160    fn from(v: Group) -> Self {
161        AssigneeEssentials {
162            id: v.id,
163            name: v.name,
164        }
165    }
166}
167
168impl From<&Group> for AssigneeEssentials {
169    fn from(v: &Group) -> Self {
170        AssigneeEssentials {
171            id: v.id,
172            name: v.name.to_owned(),
173        }
174    }
175}
176
177/// a minimal type for Redmine issues included in
178/// other Redmine objects
179#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
180pub struct IssueEssentials {
181    /// numeric id
182    pub id: u64,
183}
184
185impl From<Issue> for IssueEssentials {
186    fn from(v: Issue) -> Self {
187        IssueEssentials { id: v.id }
188    }
189}
190
191impl From<&Issue> for IssueEssentials {
192    fn from(v: &Issue) -> Self {
193        IssueEssentials { id: v.id }
194    }
195}
196
197/// the minimal data about a code repository included in other
198/// redmine objects
199#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
200pub struct RepositoryEssentials {
201    /// numeric id
202    pub id: u64,
203    /// the textual identifier
204    pub identifier: String,
205}
206
207/// the type of issue changesets
208#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
209pub struct IssueChangeset {
210    /// the revision of the changeset (e.g. commit id or number depending on VCS)
211    revision: String,
212    /// the committer
213    user: UserEssentials,
214    /// the commit message
215    comments: String,
216    /// the timestamp when this was committed
217    #[serde(
218        serialize_with = "crate::api::serialize_rfc3339",
219        deserialize_with = "crate::api::deserialize_rfc3339"
220    )]
221    committed_on: time::OffsetDateTime,
222}
223
224/// the type of journal change
225#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
226pub enum ChangePropertyType {
227    /// issue attribute change
228    #[serde(rename = "attr")]
229    Attr,
230    /// Custom field change
231    #[serde(rename = "cf")]
232    Cf,
233    /// change in issue relations
234    #[serde(rename = "relation")]
235    Relation,
236    /// change in attachments
237    #[serde(rename = "attachment")]
238    Attachment,
239}
240
241/// a changed attribute entry in a journal entry
242#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
243pub struct JournalChange {
244    /// name of the attribute
245    pub name: String,
246    /// old value
247    pub old_value: Option<String>,
248    /// new value
249    pub new_value: Option<String>,
250    /// what kind of property we are dealing with
251    pub property: ChangePropertyType,
252}
253
254/// journals (issue comments and changes)
255#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
256pub struct Journal {
257    /// numeric id
258    pub id: u64,
259    /// the author of the journal entry
260    pub user: UserEssentials,
261    /// the comment content
262    pub notes: Option<String>,
263    /// is this a private comment
264    pub private_notes: bool,
265    /// The time when this comment/change was created
266    #[serde(
267        serialize_with = "crate::api::serialize_rfc3339",
268        deserialize_with = "crate::api::deserialize_rfc3339"
269    )]
270    pub created_on: time::OffsetDateTime,
271    /// The time when this comment/change was last updated
272    #[serde(
273        serialize_with = "crate::api::serialize_rfc3339",
274        deserialize_with = "crate::api::deserialize_rfc3339"
275    )]
276    pub updated_on: time::OffsetDateTime,
277    /// The user who updated the comment/change if it differs from the author
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub updated_by: Option<UserEssentials>,
280    /// changed issue attributes
281    pub details: Vec<JournalChange>,
282}
283
284/// minimal issue used e.g. in child issues
285#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
286pub struct ChildIssue {
287    /// numeric id
288    pub id: u64,
289    /// subject
290    pub subject: String,
291    /// tracker
292    pub tracker: TrackerEssentials,
293    /// children
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub children: Option<Vec<ChildIssue>>,
296}
297
298/// a type for issue to use as an API return type
299///
300/// alternatively you can use your own type limited to the fields you need
301#[derive(Debug, Clone, Serialize, serde::Deserialize)]
302pub struct Issue {
303    /// numeric id
304    pub id: u64,
305    /// parent issue
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub parent: Option<IssueEssentials>,
308    /// the project of the issue
309    pub project: ProjectEssentials,
310    /// the tracker of the issue
311    pub tracker: TrackerEssentials,
312    /// the issue status
313    pub status: IssueStatusEssentials,
314    /// the issue priority
315    pub priority: IssuePriorityEssentials,
316    /// the issue author
317    pub author: UserEssentials,
318    /// the user or group the issue is assigned to
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub assigned_to: Option<AssigneeEssentials>,
321    /// the issue category
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub category: Option<IssueCategoryEssentials>,
324    /// the version the issue is assigned to
325    #[serde(rename = "fixed_version", skip_serializing_if = "Option::is_none")]
326    pub version: Option<VersionEssentials>,
327    /// the issue subject
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub subject: Option<String>,
330    /// the issue description
331    pub description: Option<String>,
332    /// is the issue private (only visible to roles that have the relevant permission enabled)
333    is_private: Option<bool>,
334    /// the start date for the issue
335    pub start_date: Option<time::Date>,
336    /// the due date for the issue
337    pub due_date: Option<time::Date>,
338    /// the time when the issue was closed
339    #[serde(
340        serialize_with = "crate::api::serialize_optional_rfc3339",
341        deserialize_with = "crate::api::deserialize_optional_rfc3339"
342    )]
343    pub closed_on: Option<time::OffsetDateTime>,
344    /// the percentage done
345    pub done_ratio: u64,
346    /// custom fields with values
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
349    /// estimated hours it will take to implement this issue
350    pub estimated_hours: Option<f64>,
351    /// The time when this issue was created
352    #[serde(
353        serialize_with = "crate::api::serialize_rfc3339",
354        deserialize_with = "crate::api::deserialize_rfc3339"
355    )]
356    pub created_on: time::OffsetDateTime,
357    /// The time when this issue was last updated
358    #[serde(
359        serialize_with = "crate::api::serialize_rfc3339",
360        deserialize_with = "crate::api::deserialize_rfc3339"
361    )]
362    pub updated_on: time::OffsetDateTime,
363    /// issue attachments (only when include parameter is used)
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub attachments: Option<Vec<Attachment>>,
366    /// issue relations (only when include parameter is used)
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub relations: Option<Vec<IssueRelation>>,
369    /// issue changesets (only when include parameter is used)
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub changesets: Option<Vec<IssueChangeset>>,
372    /// journal entries (comments and changes), only when include parameter is used
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub journals: Option<Vec<Journal>>,
375    /// child issues (only when include parameter is used)
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub children: Option<Vec<ChildIssue>>,
378    /// watchers
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub watchers: Option<Vec<UserEssentials>>,
381    /// the hours spent
382    #[serde(default, skip_serializing_if = "Option::is_none")]
383    pub spent_hours: Option<f64>,
384    /// the total hours spent on this and sub-tasks
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub total_spent_hours: Option<f64>,
387    /// the total hours estimated on this and sub-tasks
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub total_estimated_hours: Option<f64>,
390}
391
392/// ways to filter for subproject
393#[derive(Debug, Clone)]
394pub enum SubProjectFilter {
395    /// return no issues from subjects
396    OnlyParentProject,
397    /// return issues from a specific list of sub project ids
398    TheseSubProjects(Vec<u64>),
399    /// return issues from any but a specific list of sub project ids
400    NotTheseSubProjects(Vec<u64>),
401}
402
403impl std::fmt::Display for SubProjectFilter {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        match self {
406            SubProjectFilter::OnlyParentProject => {
407                write!(f, "!*")
408            }
409            SubProjectFilter::TheseSubProjects(ids) => {
410                let s: String = ids
411                    .iter()
412                    .map(|e| e.to_string())
413                    .collect::<Vec<_>>()
414                    .join(",");
415                write!(f, "{s}")
416            }
417            SubProjectFilter::NotTheseSubProjects(ids) => {
418                let s: String = ids
419                    .iter()
420                    .map(|e| format!("!{e}"))
421                    .collect::<Vec<_>>()
422                    .join(",");
423                write!(f, "{s}")
424            }
425        }
426    }
427}
428
429/// ways to filter for issue status
430#[derive(Debug, Clone)]
431pub enum IssueStatusFilter {
432    /// match all open statuses (default if no status filter is specified
433    Open,
434    /// match all closed statuses
435    Closed,
436    /// match both open and closed statuses
437    All,
438    /// match a specific list of statuses
439    TheseStatuses(Vec<u64>),
440    /// match any status but a specific list of statuses
441    NotTheseStatuses(Vec<u64>),
442}
443
444impl std::fmt::Display for IssueStatusFilter {
445    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446        match self {
447            IssueStatusFilter::Open => {
448                write!(f, "open")
449            }
450            IssueStatusFilter::Closed => {
451                write!(f, "closed")
452            }
453            IssueStatusFilter::All => {
454                write!(f, "*")
455            }
456            IssueStatusFilter::TheseStatuses(ids) => {
457                let s: String = ids
458                    .iter()
459                    .map(|e| e.to_string())
460                    .collect::<Vec<_>>()
461                    .join(",");
462                write!(f, "{s}")
463            }
464            IssueStatusFilter::NotTheseStatuses(ids) => {
465                let s: String = ids
466                    .iter()
467                    .map(|e| format!("!{e}"))
468                    .collect::<Vec<_>>()
469                    .join(",");
470                write!(f, "{s}")
471            }
472        }
473    }
474}
475
476/// ways to filter for users in author (always a user (not group), never !*)
477#[derive(Debug, Clone)]
478pub enum UserFilter {
479    /// match any user
480    AnyUser,
481    /// match the current API user
482    Me,
483    /// match any user but the current API user
484    NotMe,
485    /// match a specific list of users
486    TheseUsers(Vec<u64>),
487    /// match a negated specific list of users
488    NotTheseUsers(Vec<u64>),
489}
490
491impl std::fmt::Display for UserFilter {
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        match self {
494            UserFilter::AnyUser => {
495                write!(f, "*")
496            }
497            UserFilter::Me => {
498                write!(f, "me")
499            }
500            UserFilter::NotMe => {
501                write!(f, "!me")
502            }
503            UserFilter::TheseUsers(ids) => {
504                let s: String = ids
505                    .iter()
506                    .map(|e| e.to_string())
507                    .collect::<Vec<_>>()
508                    .join(",");
509                write!(f, "{s}")
510            }
511            UserFilter::NotTheseUsers(ids) => {
512                let s: String = ids
513                    .iter()
514                    .map(|e| format!("!{e}"))
515                    .collect::<Vec<_>>()
516                    .join(",");
517                write!(f, "{s}")
518            }
519        }
520    }
521}
522
523/// ways to filter for roles
524#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
525pub enum RoleFilter {
526    /// match any role
527    AnyRole,
528    /// match a specific list of role ids
529    TheseRoles(Vec<u64>),
530    /// match any role but a specific list of role ids
531    NotTheseRoles(Vec<u64>),
532}
533
534impl std::fmt::Display for RoleFilter {
535    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536        match self {
537            RoleFilter::AnyRole => {
538                write!(f, "*")
539            }
540            RoleFilter::TheseRoles(ids) => {
541                let s: String = ids
542                    .iter()
543                    .map(|e| e.to_string())
544                    .collect::<Vec<_>>()
545                    .join(",");
546                write!(f, "{s}")
547            }
548            RoleFilter::NotTheseRoles(ids) => {
549                let s: String = ids
550                    .iter()
551                    .map(|e| format!("!{e}"))
552                    .collect::<Vec<_>>()
553                    .join(",");
554                write!(f, "{s}")
555            }
556        }
557    }
558}
559
560/// ways to filter for groups the assignee is a member of
561#[derive(Debug, Clone)]
562pub enum MemberOfGroupFilter {
563    /// match any group
564    AnyGroup,
565    /// match a specific list of group ids
566    TheseGroups(Vec<u64>),
567    /// match any group but a specific list of group ids
568    NotTheseGroups(Vec<u64>),
569}
570
571impl std::fmt::Display for MemberOfGroupFilter {
572    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
573        match self {
574            MemberOfGroupFilter::AnyGroup => {
575                write!(f, "*")
576            }
577            MemberOfGroupFilter::TheseGroups(ids) => {
578                let s: String = ids
579                    .iter()
580                    .map(|e| e.to_string())
581                    .collect::<Vec<_>>()
582                    .join(",");
583                write!(f, "{s}")
584            }
585            MemberOfGroupFilter::NotTheseGroups(ids) => {
586                let s: String = ids
587                    .iter()
588                    .map(|e| format!("!{e}"))
589                    .collect::<Vec<_>>()
590                    .join(",");
591                write!(f, "{s}")
592            }
593        }
594    }
595}
596
597/// ways to filter for users or groups in assignee
598#[derive(Debug, Clone)]
599pub enum FixedVersionStatusFilter {
600    /// match open versions
601    Open,
602    /// match locked versions
603    Locked,
604    /// match closed versions
605    Closed,
606}
607
608impl std::fmt::Display for FixedVersionStatusFilter {
609    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
610        match self {
611            FixedVersionStatusFilter::Open => {
612                write!(f, "open")
613            }
614            FixedVersionStatusFilter::Locked => {
615                write!(f, "locked")
616            }
617            FixedVersionStatusFilter::Closed => {
618                write!(f, "closed")
619            }
620        }
621    }
622}
623
624/// ways to filter for users or groups in assignee
625#[derive(Debug, Clone)]
626pub enum AssigneeFilter {
627    /// match any user or group
628    AnyAssignee,
629    /// match the current API user
630    Me,
631    /// match any assignee but the current API user
632    NotMe,
633    /// match a specific list of users or groups
634    TheseAssignees(Vec<u64>),
635    /// match a negated specific list of users or groups
636    NotTheseAssignees(Vec<u64>),
637    /// match unassigned
638    NoAssignee,
639}
640
641impl std::fmt::Display for AssigneeFilter {
642    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
643        match self {
644            AssigneeFilter::AnyAssignee => {
645                write!(f, "*")
646            }
647            AssigneeFilter::Me => {
648                write!(f, "me")
649            }
650            AssigneeFilter::NotMe => {
651                write!(f, "!me")
652            }
653            AssigneeFilter::TheseAssignees(ids) => {
654                let s: String = ids
655                    .iter()
656                    .map(|e| e.to_string())
657                    .collect::<Vec<_>>()
658                    .join(",");
659                write!(f, "{s}")
660            }
661            AssigneeFilter::NotTheseAssignees(ids) => {
662                let s: String = ids
663                    .iter()
664                    .map(|e| format!("!{e}"))
665                    .collect::<Vec<_>>()
666                    .join(",");
667                write!(f, "{s}")
668            }
669            AssigneeFilter::NoAssignee => {
670                write!(f, "!*")
671            }
672        }
673    }
674}
675
676/// Filter for a custom field, consisting of its ID and a StringFieldFilter for its value.
677#[derive(Debug, Clone)]
678pub enum SortByColumn {
679    /// Sort in an ascending direction
680    Forward {
681        /// the column to sort by
682        column_name: String,
683    },
684    /// Sort in a descending direction
685    Reverse {
686        /// the column to sort by
687        column_name: String,
688    },
689}
690
691impl std::fmt::Display for SortByColumn {
692    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
693        match self {
694            SortByColumn::Forward { column_name } => {
695                write!(f, "{column_name}")
696            }
697            SortByColumn::Reverse { column_name } => {
698                write!(f, "{column_name}:desc")
699            }
700        }
701    }
702}
703
704/// The types of associated data which can be fetched along with a issue
705#[derive(Debug, Clone)]
706pub enum IssueListInclude {
707    /// Issue relations
708    Relations,
709    /// Issue time entries
710    TimeEntries,
711}
712
713impl std::fmt::Display for IssueListInclude {
714    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715        match self {
716            Self::Relations => {
717                write!(f, "relations")
718            }
719            Self::TimeEntries => {
720                write!(f, "time_entries")
721            }
722        }
723    }
724}
725
726/// The endpoint for all Redmine issues
727#[derive(Debug, Clone, Builder)]
728#[builder(setter(strip_option))]
729pub struct ListIssues {
730    /// Include associated data
731    #[builder(default)]
732    include: Option<Vec<IssueListInclude>>,
733    /// Sort by column
734    #[builder(default)]
735    sort: Option<Vec<SortByColumn>>,
736    /// Filter by issue id(s)
737    #[builder(default)]
738    issue_id: Option<Vec<u64>>,
739    /// Filter by project id
740    #[builder(default)]
741    project_id: Option<Vec<u64>>,
742    /// Filter by subproject
743    #[builder(default)]
744    subproject_id: Option<SubProjectFilter>,
745    /// Filter by tracker id
746    #[builder(default)]
747    tracker_id: Option<Vec<u64>>,
748    /// Filter by priority id
749    #[builder(default)]
750    priority_id: Option<Vec<u64>>,
751    /// Filter by parent issue id
752    #[builder(default)]
753    parent_id: Option<Vec<u64>>,
754    /// Filter by issue category id
755    #[builder(default)]
756    category_id: Option<Vec<u64>>,
757    /// Filter by issue status
758    #[builder(default)]
759    status_id: Option<IssueStatusFilter>,
760    /// Filter by subject
761    #[builder(default)]
762    subject: Option<StringFieldFilter>,
763    /// Filter by description
764    #[builder(default)]
765    description: Option<StringFieldFilter>,
766    /// Filter by notes (journal content)
767    #[builder(default)]
768    notes: Option<StringFieldFilter>,
769    /// Filter by watcher id
770    #[builder(default)]
771    watcher_id: Option<Vec<u64>>,
772    /// Filter by author
773    #[builder(default)]
774    author: Option<UserFilter>,
775    /// Filter by updated_by
776    #[builder(default)]
777    updated_by: Option<UserFilter>,
778    /// Filter by last_updated_by
779    #[builder(default)]
780    last_updated_by: Option<UserFilter>,
781    /// Filter by attachment
782    #[builder(default)]
783    attachment: Option<bool>,
784    /// Filter by attachment description
785    #[builder(default)]
786    attachment_description: Option<StringFieldFilter>,
787    /// Filter by project status
788    #[builder(default)]
789    project_status: Option<ProjectStatusFilter>,
790    /// Filter by any searchable field (free-text search)
791    #[builder(default)]
792    any_searchable: Option<StringFieldFilter>,
793    /// Filter by custom fields
794    #[builder(default)]
795    custom_field_filters: Option<Vec<CustomFieldFilter>>,
796    /// Filter by assignee
797    #[builder(default)]
798    assignee: Option<AssigneeFilter>,
799    /// Filter by a saved query
800    #[builder(default)]
801    query_id: Option<u64>,
802    /// Filter by target version
803    #[builder(default)]
804    version_id: Option<Vec<u64>>,
805    /// Filter by creation time
806    #[builder(default)]
807    created_on: Option<DateTimeFilterPast>,
808    /// Filter by update time
809    #[builder(default)]
810    updated_on: Option<DateTimeFilterPast>,
811    /// Filter by start date
812    #[builder(default)]
813    start_date: Option<DateFilter>,
814    /// Filter by due date
815    #[builder(default)]
816    due_date: Option<DateFilter>,
817    /// Filter by spent time
818    #[builder(default)]
819    spent_time: Option<FloatFilter>,
820    /// Filter by closed on date
821    #[builder(default)]
822    closed_on: Option<DateTimeFilterPast>,
823    /// Filter by estimated hours
824    #[builder(default)]
825    estimated_hours: Option<FloatFilter>,
826    /// Filter by done ratio
827    #[builder(default)]
828    done_ratio: Option<IntegerFilter>,
829    /// Filter by author group
830    #[builder(default)]
831    author_group: Option<MemberOfGroupFilter>,
832    /// Filter by author role
833    #[builder(default)]
834    author_role: Option<RoleFilter>,
835    /// Filter by groups the assignee is a member of
836    #[builder(default)]
837    member_of_group: Option<MemberOfGroupFilter>,
838    /// Filter by roles the assignee has
839    #[builder(default)]
840    assigned_to_role: Option<RoleFilter>,
841    /// Filter by fixed version due date
842    #[builder(default)]
843    fixed_version_due_date: Option<DateFilter>,
844    /// Filter by fixed version status
845    #[builder(default)]
846    fixed_version_status: Option<FixedVersionStatusFilter>,
847    /// Filter by child issue id
848    #[builder(default)]
849    child_id: Option<Vec<u64>>,
850    /// Filter by private status
851    #[builder(default)]
852    is_private: Option<bool>,
853}
854
855impl ReturnsJsonResponse for ListIssues {}
856
857impl Pageable for ListIssues {
858    fn response_wrapper_key(&self) -> String {
859        "issues".to_string()
860    }
861}
862
863impl ListIssues {
864    /// Create a builder for the endpoint.
865    #[must_use]
866    pub fn builder() -> ListIssuesBuilder {
867        ListIssuesBuilder::default()
868    }
869}
870
871impl Endpoint for ListIssues {
872    fn method(&self) -> Method {
873        Method::GET
874    }
875
876    fn endpoint(&self) -> Cow<'static, str> {
877        "issues.json".into()
878    }
879
880    fn parameters(&self) -> QueryParams<'_> {
881        let mut params = QueryParams::default();
882        params.push_opt("include", self.include.as_ref());
883        params.push_opt("sort", self.sort.as_ref());
884        params.push_opt("issue_id", self.issue_id.as_ref());
885        params.push_opt("project_id", self.project_id.as_ref());
886        params.push_opt(
887            "subproject_id",
888            self.subproject_id.as_ref().map(|s| s.to_string()),
889        );
890        params.push_opt("tracker_id", self.tracker_id.as_ref());
891        params.push_opt("priority_id", self.priority_id.as_ref());
892        params.push_opt("parent_id", self.parent_id.as_ref());
893        params.push_opt("category_id", self.category_id.as_ref());
894        params.push_opt("status_id", self.status_id.as_ref().map(|s| s.to_string()));
895        params.push_opt("subject", self.subject.as_ref().map(|s| s.to_string()));
896        params.push_opt(
897            "description",
898            self.description.as_ref().map(|s| s.to_string()),
899        );
900        params.push_opt("notes", self.notes.as_ref().map(|s| s.to_string()));
901        params.push_opt("author_id", self.author.as_ref().map(|s| s.to_string()));
902        params.push_opt(
903            "updated_by_id",
904            self.updated_by.as_ref().map(|s| s.to_string()),
905        );
906        params.push_opt(
907            "last_updated_by_id",
908            self.last_updated_by.as_ref().map(|s| s.to_string()),
909        );
910        params.push_opt("attachment", self.attachment);
911        params.push_opt(
912            "attachment_description",
913            self.attachment_description.as_ref().map(|s| s.to_string()),
914        );
915        params.push_opt(
916            "project_status",
917            self.project_status.as_ref().map(|s| s.to_string()),
918        );
919        params.push_opt(
920            "any_searchable",
921            self.any_searchable.as_ref().map(|s| s.to_string()),
922        );
923        if let Some(filters) = self.custom_field_filters.as_ref() {
924            for filter in filters {
925                params.push(format!("cf_{}", filter.id), filter.value.to_string());
926            }
927        }
928        params.push_opt(
929            "assigned_to_id",
930            self.assignee.as_ref().map(|s| s.to_string()),
931        );
932        params.push_opt("query_id", self.query_id);
933        params.push_opt("fixed_version_id", self.version_id.as_ref());
934        params.push_opt(
935            "created_on",
936            self.created_on.as_ref().map(|s| s.to_string()),
937        );
938        params.push_opt(
939            "updated_on",
940            self.updated_on.as_ref().map(|s| s.to_string()),
941        );
942        params.push_opt(
943            "start_date",
944            self.start_date.as_ref().map(|s| s.to_string()),
945        );
946        params.push_opt("due_date", self.due_date.as_ref().map(|s| s.to_string()));
947        params.push_opt(
948            "spent_time",
949            self.spent_time.as_ref().map(|s| s.to_string()),
950        );
951        params.push_opt("closed_on", self.closed_on.as_ref().map(|s| s.to_string()));
952        params.push_opt(
953            "estimated_hours",
954            self.estimated_hours.as_ref().map(|s| s.to_string()),
955        );
956        params.push_opt(
957            "done_ratio",
958            self.done_ratio.as_ref().map(|s| s.to_string()),
959        );
960        params.push_opt(
961            "author.group",
962            self.author_group.as_ref().map(|s| s.to_string()),
963        );
964        params.push_opt(
965            "author.role",
966            self.author_role.as_ref().map(|s| s.to_string()),
967        );
968        params.push_opt(
969            "member_of_group",
970            self.member_of_group.as_ref().map(|s| s.to_string()),
971        );
972        params.push_opt(
973            "assigned_to_role",
974            self.assigned_to_role.as_ref().map(|s| s.to_string()),
975        );
976        params.push_opt(
977            "fixed_version.due_date",
978            self.fixed_version_due_date.as_ref().map(|s| s.to_string()),
979        );
980        params.push_opt(
981            "fixed_version.status",
982            self.fixed_version_status.as_ref().map(|s| s.to_string()),
983        );
984        params.push_opt("child_id", self.child_id.as_ref());
985        params.push_opt("is_private", self.is_private);
986        params.push_opt(
987            "watcher_id",
988            self.watcher_id.as_ref().map(|ids| {
989                ids.iter()
990                    .map(std::string::ToString::to_string)
991                    .collect::<Vec<String>>()
992                    .join(",")
993            }),
994        );
995        params
996    }
997}
998
999/// The types of associated data which can be fetched along with a issue
1000#[derive(Debug, Clone)]
1001pub enum IssueInclude {
1002    /// Child issues
1003    Children,
1004    /// Issue attachments
1005    Attachments,
1006    /// Issue relations
1007    Relations,
1008    /// VCS changesets
1009    Changesets,
1010    /// the notes and changes to the issue
1011    Journals,
1012    /// Users watching the issue
1013    Watchers,
1014    /// The statuses this issue can transition to
1015    ///
1016    /// This can be influenced by
1017    ///
1018    /// - the defined workflow
1019    ///   - the issue's current tracker
1020    ///   - the issue's current status
1021    ///   - the member's role
1022    /// - the existence of any open subtask(s)
1023    /// - the existence of any open blocking issue(s)
1024    /// - the existence of a closed parent issue
1025    ///
1026    AllowedStatuses,
1027}
1028
1029impl std::fmt::Display for IssueInclude {
1030    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1031        match self {
1032            Self::Children => {
1033                write!(f, "children")
1034            }
1035            Self::Attachments => {
1036                write!(f, "attachments")
1037            }
1038            Self::Relations => {
1039                write!(f, "relations")
1040            }
1041            Self::Changesets => {
1042                write!(f, "changesets")
1043            }
1044            Self::Journals => {
1045                write!(f, "journals")
1046            }
1047            Self::Watchers => {
1048                write!(f, "watchers")
1049            }
1050            Self::AllowedStatuses => {
1051                write!(f, "allowed_statuses")
1052            }
1053        }
1054    }
1055}
1056
1057/// The endpoint for a specific Redmine issue
1058#[derive(Debug, Clone, Builder)]
1059#[builder(setter(strip_option))]
1060pub struct GetIssue {
1061    /// id of the issue to retrieve
1062    id: u64,
1063    /// associated data to include
1064    #[builder(default)]
1065    include: Option<Vec<IssueInclude>>,
1066}
1067
1068impl ReturnsJsonResponse for GetIssue {}
1069impl NoPagination for GetIssue {}
1070
1071impl GetIssue {
1072    /// Create a builder for the endpoint.
1073    #[must_use]
1074    pub fn builder() -> GetIssueBuilder {
1075        GetIssueBuilder::default()
1076    }
1077}
1078
1079impl Endpoint for GetIssue {
1080    fn method(&self) -> Method {
1081        Method::GET
1082    }
1083
1084    fn endpoint(&self) -> Cow<'static, str> {
1085        format!("issues/{}.json", &self.id).into()
1086    }
1087
1088    fn parameters(&self) -> QueryParams<'_> {
1089        let mut params = QueryParams::default();
1090        params.push_opt("include", self.include.as_ref());
1091        params
1092    }
1093}
1094
1095/// the information the uploader needs to supply for an attachment
1096/// in [CreateIssue] or [UpdateIssue]
1097#[derive(Debug, Clone, Serialize)]
1098pub struct UploadedAttachment<'a> {
1099    /// the upload token from [UploadFile|crate::api::uploads::UploadFile]
1100    pub token: Cow<'a, str>,
1101    /// the filename
1102    pub filename: Cow<'a, str>,
1103    /// a description for the file
1104    #[serde(skip_serializing_if = "Option::is_none")]
1105    pub description: Option<Cow<'a, str>>,
1106    /// the MIME content type of the file
1107    pub content_type: Cow<'a, str>,
1108}
1109
1110/// The endpoint to create a Redmine issue
1111#[serde_with::skip_serializing_none]
1112#[derive(Debug, Clone, Builder, Serialize)]
1113#[builder(setter(strip_option))]
1114pub struct CreateIssue<'a> {
1115    /// project for the issue
1116    project_id: u64,
1117    /// tracker for the issue
1118    #[builder(default)]
1119    tracker_id: Option<u64>,
1120    /// status of the issue
1121    #[builder(default)]
1122    status_id: Option<u64>,
1123    /// issue priority
1124    #[builder(default)]
1125    priority_id: Option<u64>,
1126    /// issue subject
1127    #[builder(setter(into), default)]
1128    subject: Option<Cow<'a, str>>,
1129    /// issue description
1130    #[builder(setter(into), default)]
1131    description: Option<Cow<'a, str>>,
1132    /// issue category
1133    #[builder(default)]
1134    category_id: Option<u64>,
1135    /// ID of the Target Versions (previously called 'Fixed Version' and still referred to as such in the API)
1136    #[builder(default, setter(name = "version"))]
1137    fixed_version_id: Option<u64>,
1138    /// user/group id the issue will be assigned to
1139    #[builder(default)]
1140    assigned_to_id: Option<u64>,
1141    /// Id of the parent issue
1142    #[builder(default)]
1143    parent_issue_id: Option<u64>,
1144    /// custom field values
1145    #[builder(default)]
1146    custom_fields: Option<Vec<CustomField<'a>>>,
1147    /// user ids of watchers of the issue
1148    #[builder(default)]
1149    watcher_user_ids: Option<Vec<u64>>,
1150    /// is the issue private (only visible to roles that have the relevant permission enabled)
1151    #[builder(default)]
1152    is_private: Option<bool>,
1153    /// estimated hours it will take to implement this issue
1154    #[builder(default)]
1155    estimated_hours: Option<f64>,
1156    /// attachments (files)
1157    #[builder(default)]
1158    uploads: Option<Vec<UploadedAttachment<'a>>>,
1159}
1160
1161impl<'a> CreateIssue<'a> {
1162    /// Create a builder for the endpoint.
1163    #[must_use]
1164    pub fn builder() -> CreateIssueBuilder<'a> {
1165        CreateIssueBuilder::default()
1166    }
1167}
1168
1169impl ReturnsJsonResponse for CreateIssue<'_> {}
1170impl NoPagination for CreateIssue<'_> {}
1171
1172impl Endpoint for CreateIssue<'_> {
1173    fn method(&self) -> Method {
1174        Method::POST
1175    }
1176
1177    fn endpoint(&self) -> Cow<'static, str> {
1178        "issues.json".into()
1179    }
1180
1181    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1182        Ok(Some((
1183            "application/json",
1184            serde_json::to_vec(&IssueWrapper::<CreateIssue> {
1185                issue: (*self).to_owned(),
1186            })?,
1187        )))
1188    }
1189}
1190
1191/// The endpoint to update an existing Redmine issue
1192#[serde_with::skip_serializing_none]
1193#[derive(Debug, Clone, Builder, Serialize)]
1194#[builder(setter(strip_option))]
1195pub struct UpdateIssue<'a> {
1196    /// id of the issue to update
1197    #[serde(skip_serializing)]
1198    id: u64,
1199    /// project for the issue
1200    #[builder(default)]
1201    project_id: Option<u64>,
1202    /// tracker for the issue
1203    #[builder(default)]
1204    tracker_id: Option<u64>,
1205    /// status of the issue
1206    #[builder(default)]
1207    status_id: Option<u64>,
1208    /// issue priority
1209    #[builder(default)]
1210    priority_id: Option<u64>,
1211    /// issue subject
1212    #[builder(setter(into), default)]
1213    subject: Option<Cow<'a, str>>,
1214    /// issue description
1215    #[builder(setter(into), default)]
1216    description: Option<Cow<'a, str>>,
1217    /// issue category
1218    #[builder(default)]
1219    category_id: Option<u64>,
1220    /// ID of the Target Versions (previously called 'Fixed Version' and still referred to as such in the API)
1221    #[builder(default, setter(name = "version"))]
1222    fixed_version_id: Option<u64>,
1223    /// user/group id the issue will be assigned to
1224    #[builder(default)]
1225    assigned_to_id: Option<u64>,
1226    /// Id of the parent issue
1227    #[builder(default)]
1228    parent_issue_id: Option<u64>,
1229    /// custom field values
1230    #[builder(default)]
1231    custom_fields: Option<Vec<CustomField<'a>>>,
1232    /// user ids of watchers of the issue
1233    #[builder(default)]
1234    watcher_user_ids: Option<Vec<u64>>,
1235    /// is the issue private (only visible to roles that have the relevant permission enabled)
1236    #[builder(default)]
1237    is_private: Option<bool>,
1238    /// estimated hours it will take to implement this issue
1239    #[builder(default)]
1240    estimated_hours: Option<f64>,
1241    /// add a comment (note)
1242    #[builder(default)]
1243    notes: Option<Cow<'a, str>>,
1244    /// is the added comment/note private
1245    #[builder(default)]
1246    private_notes: Option<bool>,
1247    /// attachments (files)
1248    #[builder(default)]
1249    uploads: Option<Vec<UploadedAttachment<'a>>>,
1250}
1251
1252impl<'a> UpdateIssue<'a> {
1253    /// Create a builder for the endpoint.
1254    #[must_use]
1255    pub fn builder() -> UpdateIssueBuilder<'a> {
1256        UpdateIssueBuilder::default()
1257    }
1258}
1259
1260impl Endpoint for UpdateIssue<'_> {
1261    fn method(&self) -> Method {
1262        Method::PUT
1263    }
1264
1265    fn endpoint(&self) -> Cow<'static, str> {
1266        format!("issues/{}.json", self.id).into()
1267    }
1268
1269    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1270        Ok(Some((
1271            "application/json",
1272            serde_json::to_vec(&IssueWrapper::<UpdateIssue> {
1273                issue: (*self).to_owned(),
1274            })?,
1275        )))
1276    }
1277}
1278
1279/// The endpoint to delete a Redmine issue
1280#[derive(Debug, Clone, Builder)]
1281#[builder(setter(strip_option))]
1282pub struct DeleteIssue {
1283    /// id of the issue to delete
1284    id: u64,
1285}
1286
1287impl DeleteIssue {
1288    /// Create a builder for the endpoint.
1289    #[must_use]
1290    pub fn builder() -> DeleteIssueBuilder {
1291        DeleteIssueBuilder::default()
1292    }
1293}
1294
1295impl Endpoint for DeleteIssue {
1296    fn method(&self) -> Method {
1297        Method::DELETE
1298    }
1299
1300    fn endpoint(&self) -> Cow<'static, str> {
1301        format!("issues/{}.json", &self.id).into()
1302    }
1303}
1304
1305/// The endpoint to add a Redmine user as a watcher on a Redmine issue
1306#[derive(Debug, Clone, Builder, Serialize)]
1307#[builder(setter(strip_option))]
1308pub struct AddWatcher {
1309    /// id of the issue to add the watcher to
1310    #[serde(skip_serializing)]
1311    issue_id: u64,
1312    /// id of the user to add as a watcher
1313    user_id: u64,
1314}
1315
1316impl AddWatcher {
1317    /// Create a builder for the endpoint.
1318    #[must_use]
1319    pub fn builder() -> AddWatcherBuilder {
1320        AddWatcherBuilder::default()
1321    }
1322}
1323
1324impl Endpoint for AddWatcher {
1325    fn method(&self) -> Method {
1326        Method::POST
1327    }
1328
1329    fn endpoint(&self) -> Cow<'static, str> {
1330        format!("issues/{}/watchers.json", &self.issue_id).into()
1331    }
1332
1333    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1334        Ok(Some(("application/json", serde_json::to_vec(self)?)))
1335    }
1336}
1337
1338/// The endpoint to remove a Redmine user from a Redmine issue as a watcher
1339#[derive(Debug, Clone, Builder)]
1340#[builder(setter(strip_option))]
1341pub struct RemoveWatcher {
1342    /// id of the issue to remove the watcher from
1343    issue_id: u64,
1344    /// id of the user to remove as a watcher
1345    user_id: u64,
1346}
1347
1348impl RemoveWatcher {
1349    /// Create a builder for the endpoint.
1350    #[must_use]
1351    pub fn builder() -> RemoveWatcherBuilder {
1352        RemoveWatcherBuilder::default()
1353    }
1354}
1355
1356impl Endpoint for RemoveWatcher {
1357    fn method(&self) -> Method {
1358        Method::DELETE
1359    }
1360
1361    fn endpoint(&self) -> Cow<'static, str> {
1362        format!("issues/{}/watchers/{}.json", &self.issue_id, &self.user_id).into()
1363    }
1364}
1365
1366/// helper struct for outer layers with a issues field holding the inner data
1367#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
1368pub struct IssuesWrapper<T> {
1369    /// to parse JSON with issues key
1370    pub issues: Vec<T>,
1371}
1372
1373/// A lot of APIs in Redmine wrap their data in an extra layer, this is a
1374/// helper struct for outer layers with a issue field holding the inner data
1375#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
1376pub struct IssueWrapper<T> {
1377    /// to parse JSON with an issue key
1378    pub issue: T,
1379}
1380
1381#[cfg(test)]
1382pub(crate) mod test {
1383    use super::*;
1384    use crate::api::ResponsePage;
1385    use crate::api::test_helpers::with_project;
1386    use pretty_assertions::assert_eq;
1387    use std::error::Error;
1388    use tokio::sync::RwLock;
1389    use tracing_test::traced_test;
1390
1391    /// needed so we do not get 404s when listing while
1392    /// creating/deleting or creating/updating/deleting
1393    pub static ISSUES_LOCK: RwLock<()> = RwLock::const_new(());
1394
1395    #[traced_test]
1396    #[test]
1397    fn test_list_issues_first_page() -> Result<(), Box<dyn Error>> {
1398        let _r_issues = ISSUES_LOCK.blocking_read();
1399        dotenvy::dotenv()?;
1400        let redmine = crate::api::Redmine::from_env(
1401            reqwest::blocking::Client::builder()
1402                .use_rustls_tls()
1403                .build()?,
1404        )?;
1405        let endpoint = ListIssues::builder().build()?;
1406        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1407        Ok(())
1408    }
1409
1410    /// this version of the test will load all pages of issues which means it
1411    /// can take a while (a minute or more) so you need to use --include-ignored
1412    /// or --ignored to run it
1413    #[traced_test]
1414    #[test]
1415    #[ignore]
1416    fn test_list_issues_all_pages() -> Result<(), Box<dyn Error>> {
1417        let _r_issues = ISSUES_LOCK.blocking_read();
1418        dotenvy::dotenv()?;
1419        let redmine = crate::api::Redmine::from_env(
1420            reqwest::blocking::Client::builder()
1421                .use_rustls_tls()
1422                .build()?,
1423        )?;
1424        let endpoint = ListIssues::builder().build()?;
1425        redmine.json_response_body_all_pages::<_, Issue>(&endpoint)?;
1426        Ok(())
1427    }
1428
1429    /// this version of the test will load all pages of issues which means it
1430    /// can take a while (a minute or more) so you need to use --include-ignored
1431    /// or --ignored to run it
1432    #[traced_test]
1433    #[test]
1434    #[ignore]
1435    fn test_list_issues_all_pages_iter() -> Result<(), Box<dyn Error>> {
1436        let _r_issues = ISSUES_LOCK.blocking_read();
1437        dotenvy::dotenv()?;
1438        let redmine = crate::api::Redmine::from_env(
1439            reqwest::blocking::Client::builder()
1440                .use_rustls_tls()
1441                .build()?,
1442        )?;
1443        let endpoint = ListIssues::builder().build()?;
1444        let mut i = 0;
1445        for issue in redmine.json_response_body_all_pages_iter::<_, Issue>(&endpoint) {
1446            let _issue = issue?;
1447            i += 1;
1448        }
1449        assert!(i > 0);
1450
1451        Ok(())
1452    }
1453
1454    /// this version of the test will load all pages of issues which means it
1455    /// can take a while (a minute or more) so you need to use --include-ignored
1456    /// or --ignored to run it
1457    #[traced_test]
1458    #[tokio::test]
1459    #[ignore]
1460    async fn test_list_issues_all_pages_stream() -> Result<(), Box<dyn Error>> {
1461        let _r_issues = ISSUES_LOCK.read().await;
1462        dotenvy::dotenv()?;
1463        let redmine = crate::api::RedmineAsync::from_env(
1464            reqwest::Client::builder().use_rustls_tls().build()?,
1465        )?;
1466        let endpoint = ListIssues::builder().build()?;
1467        let mut i = 0;
1468        let mut stream = redmine.json_response_body_all_pages_stream::<_, Issue>(&endpoint);
1469        while let Some(issue) = <_ as futures::stream::StreamExt>::next(&mut stream).await {
1470            let _issue = issue?;
1471            i += 1;
1472        }
1473        assert!(i > 0);
1474
1475        Ok(())
1476    }
1477
1478    #[traced_test]
1479    #[test]
1480    fn test_get_issue() -> Result<(), Box<dyn Error>> {
1481        let _r_issues = ISSUES_LOCK.blocking_read();
1482        dotenvy::dotenv()?;
1483        let redmine = crate::api::Redmine::from_env(
1484            reqwest::blocking::Client::builder()
1485                .use_rustls_tls()
1486                .build()?,
1487        )?;
1488        let endpoint = GetIssue::builder().id(40000).build()?;
1489        redmine.json_response_body::<_, IssueWrapper<Issue>>(&endpoint)?;
1490        Ok(())
1491    }
1492
1493    #[function_name::named]
1494    #[traced_test]
1495    #[test]
1496    fn test_create_issue() -> Result<(), Box<dyn Error>> {
1497        let _w_issues = ISSUES_LOCK.blocking_write();
1498        let name = format!("unittest_{}", function_name!());
1499        with_project(&name, |redmine, project_id, _| {
1500            let create_endpoint = super::CreateIssue::builder()
1501                .project_id(project_id)
1502                .subject("old test subject")
1503                .build()?;
1504            redmine.json_response_body::<_, IssueWrapper<Issue>>(&create_endpoint)?;
1505            Ok(())
1506        })?;
1507        Ok(())
1508    }
1509
1510    #[function_name::named]
1511    #[traced_test]
1512    #[test]
1513    fn test_update_issue() -> Result<(), Box<dyn Error>> {
1514        let _w_issues = ISSUES_LOCK.blocking_write();
1515        let name = format!("unittest_{}", function_name!());
1516        with_project(&name, |redmine, project_id, _name| {
1517            let create_endpoint = super::CreateIssue::builder()
1518                .project_id(project_id)
1519                .subject("old test subject")
1520                .build()?;
1521            let IssueWrapper { issue }: IssueWrapper<Issue> =
1522                redmine.json_response_body::<_, _>(&create_endpoint)?;
1523            let update_endpoint = super::UpdateIssue::builder()
1524                .id(issue.id)
1525                .subject("New test subject")
1526                .build()?;
1527            redmine.ignore_response_body::<_>(&update_endpoint)?;
1528            Ok(())
1529        })?;
1530        Ok(())
1531    }
1532
1533    #[function_name::named]
1534    #[traced_test]
1535    #[test]
1536    fn test_delete_issue() -> Result<(), Box<dyn Error>> {
1537        let _w_issues = ISSUES_LOCK.blocking_write();
1538        let name = format!("unittest_{}", function_name!());
1539        with_project(&name, |redmine, project_id, _name| {
1540            let create_endpoint = super::CreateIssue::builder()
1541                .project_id(project_id)
1542                .subject("test subject")
1543                .build()?;
1544            let IssueWrapper { issue }: IssueWrapper<Issue> =
1545                redmine.json_response_body::<_, _>(&create_endpoint)?;
1546            let delete_endpoint = super::DeleteIssue::builder().id(issue.id).build()?;
1547            redmine.ignore_response_body::<_>(&delete_endpoint)?;
1548            Ok(())
1549        })?;
1550        Ok(())
1551    }
1552
1553    /// this tests if any of the results contain a field we are not deserializing
1554    ///
1555    /// this will only catch fields we missed if they are part of the response but
1556    /// it is better than nothing
1557    #[traced_test]
1558    #[test]
1559    fn test_completeness_issue_type_first_page() -> Result<(), Box<dyn Error>> {
1560        let _r_issues = ISSUES_LOCK.blocking_read();
1561        dotenvy::dotenv()?;
1562        let redmine = crate::api::Redmine::from_env(
1563            reqwest::blocking::Client::builder()
1564                .use_rustls_tls()
1565                .build()?,
1566        )?;
1567        let endpoint = ListIssues::builder()
1568            .include(vec![
1569                IssueListInclude::TimeEntries,
1570                IssueListInclude::Relations,
1571            ])
1572            .build()?;
1573        let ResponsePage {
1574            values,
1575            total_count: _,
1576            offset: _,
1577            limit: _,
1578        } = redmine.json_response_body_page::<_, serde_json::Value>(&endpoint, 0, 100)?;
1579        for value in values {
1580            let o: Issue = serde_json::from_value(value.clone())?;
1581            let reserialized = serde_json::to_value(o)?;
1582            let expected_value = if let serde_json::Value::Object(obj) = value {
1583                let mut expected_obj = obj.clone();
1584                if obj
1585                    .get("total_estimated_hours")
1586                    .is_some_and(|v| *v == serde_json::Value::Null)
1587                {
1588                    expected_obj.remove("total_estimated_hours");
1589                }
1590                serde_json::Value::Object(expected_obj)
1591            } else {
1592                value
1593            };
1594            assert_eq!(expected_value, reserialized);
1595        }
1596        Ok(())
1597    }
1598
1599    /// this tests if any of the results contain a field we are not deserializing
1600    ///
1601    /// this will only catch fields we missed if they are part of the response but
1602    /// it is better than nothing
1603    ///
1604    /// this version of the test will load all pages of issues which means it
1605    /// can take a while (a minute or more) so you need to use --include-ignored
1606    /// or --ignored to run it
1607    #[traced_test]
1608    #[test]
1609    #[ignore]
1610    fn test_completeness_issue_type_all_pages() -> Result<(), Box<dyn Error>> {
1611        let _r_issues = ISSUES_LOCK.blocking_read();
1612        dotenvy::dotenv()?;
1613        let redmine = crate::api::Redmine::from_env(
1614            reqwest::blocking::Client::builder()
1615                .use_rustls_tls()
1616                .build()?,
1617        )?;
1618        let endpoint = ListIssues::builder()
1619            .include(vec![
1620                IssueListInclude::TimeEntries,
1621                IssueListInclude::Relations,
1622            ])
1623            .build()?;
1624        let values = redmine.json_response_body_all_pages::<_, serde_json::Value>(&endpoint)?;
1625        for value in values {
1626            let o: Issue = serde_json::from_value(value.clone())?;
1627            let reserialized = serde_json::to_value(o)?;
1628            let expected_value = if let serde_json::Value::Object(obj) = value {
1629                let mut expected_obj = obj.clone();
1630                if obj
1631                    .get("total_estimated_hours")
1632                    .is_some_and(|v| *v == serde_json::Value::Null)
1633                {
1634                    expected_obj.remove("total_estimated_hours");
1635                }
1636                serde_json::Value::Object(expected_obj)
1637            } else {
1638                value
1639            };
1640            assert_eq!(expected_value, reserialized);
1641        }
1642        Ok(())
1643    }
1644
1645    /// this tests if any of the results contain a field we are not deserializing
1646    ///
1647    /// this will only catch fields we missed if they are part of the response but
1648    /// it is better than nothing
1649    ///
1650    /// this version of the test will load all pages of issues and the individual
1651    /// issues for each via GetIssue which means it
1652    /// can take a while (about 400 seconds) so you need to use --include-ignored
1653    /// or --ignored to run it
1654    #[traced_test]
1655    #[test]
1656    #[ignore]
1657    fn test_completeness_issue_type_all_pages_all_issue_details() -> Result<(), Box<dyn Error>> {
1658        let _r_issues = ISSUES_LOCK.blocking_read();
1659        dotenvy::dotenv()?;
1660        let redmine = crate::api::Redmine::from_env(
1661            reqwest::blocking::Client::builder()
1662                .use_rustls_tls()
1663                .build()?,
1664        )?;
1665        let endpoint = ListIssues::builder()
1666            .include(vec![
1667                IssueListInclude::TimeEntries,
1668                IssueListInclude::Relations,
1669            ])
1670            .build()?;
1671        let issues = redmine.json_response_body_all_pages::<_, Issue>(&endpoint)?;
1672        for issue in issues {
1673            let get_endpoint = GetIssue::builder()
1674                .id(issue.id)
1675                .include(vec![
1676                    IssueInclude::Attachments,
1677                    IssueInclude::Children,
1678                    IssueInclude::Changesets,
1679                    IssueInclude::Relations,
1680                    IssueInclude::Journals,
1681                    IssueInclude::Watchers,
1682                ])
1683                .build()?;
1684            let IssueWrapper { issue: mut value } =
1685                redmine.json_response_body::<_, IssueWrapper<serde_json::Value>>(&get_endpoint)?;
1686            let o: Issue = serde_json::from_value(value.clone())?;
1687            // workaround for the fact that the field total_estimated_hours is put into the result
1688            // when its null in the GetIssue endpoint but not in the ListIssues one
1689            // we can only do one or the other in our JSON serialization unless we want to add
1690            // extra fields just to keep track of the missing field vs. field with null value
1691            // difference
1692            let value_object = value.as_object_mut().unwrap();
1693            if value_object.get("total_estimated_hours") == Some(&serde_json::Value::Null) {
1694                value_object.remove("total_estimated_hours");
1695            }
1696            let reserialized = serde_json::to_value(o)?;
1697            assert_eq!(value, reserialized);
1698        }
1699        Ok(())
1700    }
1701
1702    #[traced_test]
1703    #[test]
1704    fn test_list_issues_created_on_filter_exact_match() -> Result<(), Box<dyn Error>> {
1705        let _r_issues = ISSUES_LOCK.blocking_read();
1706        dotenvy::dotenv()?;
1707        let redmine = crate::api::Redmine::from_env(
1708            reqwest::blocking::Client::builder()
1709                .use_rustls_tls()
1710                .build()?,
1711        )?;
1712
1713        let dt = time::macros::datetime!(2023-01-15 10:30:00 UTC);
1714        let endpoint = ListIssues::builder()
1715            .created_on(DateTimeFilterPast::ExactMatch(dt))
1716            .build()?;
1717        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1718
1719        Ok(())
1720    }
1721
1722    #[traced_test]
1723    #[test]
1724    fn test_list_issues_created_on_filter_range() -> Result<(), Box<dyn Error>> {
1725        let _r_issues = ISSUES_LOCK.blocking_read();
1726        dotenvy::dotenv()?;
1727        let redmine = crate::api::Redmine::from_env(
1728            reqwest::blocking::Client::builder()
1729                .use_rustls_tls()
1730                .build()?,
1731        )?;
1732
1733        let dt_start = time::macros::datetime!(2023-01-01 00:00:00 UTC);
1734        let dt_end = time::macros::datetime!(2023-01-31 23:59:59 UTC);
1735        let endpoint = ListIssues::builder()
1736            .created_on(DateTimeFilterPast::Range(dt_start, dt_end))
1737            .build()?;
1738        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1739
1740        Ok(())
1741    }
1742
1743    #[traced_test]
1744    #[test]
1745    fn test_list_issues_created_on_filter_less_than_or_equal() -> Result<(), Box<dyn Error>> {
1746        let _r_issues = ISSUES_LOCK.blocking_read();
1747        dotenvy::dotenv()?;
1748        let redmine = crate::api::Redmine::from_env(
1749            reqwest::blocking::Client::builder()
1750                .use_rustls_tls()
1751                .build()?,
1752        )?;
1753
1754        let dt = time::macros::datetime!(2023-01-15 10:30:00 UTC);
1755        let endpoint = ListIssues::builder()
1756            .created_on(DateTimeFilterPast::LessThanOrEqual(dt))
1757            .build()?;
1758        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1759
1760        Ok(())
1761    }
1762
1763    #[traced_test]
1764    #[test]
1765    fn test_list_issues_created_on_filter_greater_than_or_equal() -> Result<(), Box<dyn Error>> {
1766        let _r_issues = ISSUES_LOCK.blocking_read();
1767        dotenvy::dotenv()?;
1768        let redmine = crate::api::Redmine::from_env(
1769            reqwest::blocking::Client::builder()
1770                .use_rustls_tls()
1771                .build()?,
1772        )?;
1773
1774        let dt = time::macros::datetime!(2023-01-15 10:30:00 UTC);
1775        let endpoint = ListIssues::builder()
1776            .created_on(DateTimeFilterPast::GreaterThanOrEqual(dt))
1777            .build()?;
1778        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1779
1780        Ok(())
1781    }
1782
1783    #[traced_test]
1784    #[test]
1785    fn test_list_issues_created_on_filter_less_than_days_ago() -> Result<(), Box<dyn Error>> {
1786        let _r_issues = ISSUES_LOCK.blocking_read();
1787        dotenvy::dotenv()?;
1788        let redmine = crate::api::Redmine::from_env(
1789            reqwest::blocking::Client::builder()
1790                .use_rustls_tls()
1791                .build()?,
1792        )?;
1793
1794        let endpoint = ListIssues::builder()
1795            .created_on(DateTimeFilterPast::LessThanDaysAgo(5))
1796            .build()?;
1797        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1798
1799        Ok(())
1800    }
1801
1802    #[traced_test]
1803    #[test]
1804    fn test_list_issues_created_on_filter_more_than_days_ago() -> Result<(), Box<dyn Error>> {
1805        let _r_issues = ISSUES_LOCK.blocking_read();
1806        dotenvy::dotenv()?;
1807        let redmine = crate::api::Redmine::from_env(
1808            reqwest::blocking::Client::builder()
1809                .use_rustls_tls()
1810                .build()?,
1811        )?;
1812
1813        let endpoint = ListIssues::builder()
1814            .created_on(DateTimeFilterPast::MoreThanDaysAgo(10))
1815            .build()?;
1816        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1817
1818        Ok(())
1819    }
1820
1821    #[traced_test]
1822    #[test]
1823    fn test_list_issues_created_on_filter_within_past_days() -> Result<(), Box<dyn Error>> {
1824        let _r_issues = ISSUES_LOCK.blocking_read();
1825        dotenvy::dotenv()?;
1826        let redmine = crate::api::Redmine::from_env(
1827            reqwest::blocking::Client::builder()
1828                .use_rustls_tls()
1829                .build()?,
1830        )?;
1831
1832        let endpoint = ListIssues::builder()
1833            .created_on(DateTimeFilterPast::WithinPastDays(7))
1834            .build()?;
1835        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1836
1837        Ok(())
1838    }
1839
1840    #[traced_test]
1841    #[test]
1842    fn test_list_issues_created_on_filter_exact_days_ago() -> Result<(), Box<dyn Error>> {
1843        let _r_issues = ISSUES_LOCK.blocking_read();
1844        dotenvy::dotenv()?;
1845        let redmine = crate::api::Redmine::from_env(
1846            reqwest::blocking::Client::builder()
1847                .use_rustls_tls()
1848                .build()?,
1849        )?;
1850
1851        let endpoint = ListIssues::builder()
1852            .created_on(DateTimeFilterPast::ExactDaysAgo(3))
1853            .build()?;
1854        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1855
1856        Ok(())
1857    }
1858
1859    #[traced_test]
1860    #[test]
1861    fn test_list_issues_created_on_filter_today() -> Result<(), Box<dyn Error>> {
1862        let _r_issues = ISSUES_LOCK.blocking_read();
1863        dotenvy::dotenv()?;
1864        let redmine = crate::api::Redmine::from_env(
1865            reqwest::blocking::Client::builder()
1866                .use_rustls_tls()
1867                .build()?,
1868        )?;
1869
1870        let endpoint = ListIssues::builder()
1871            .created_on(DateTimeFilterPast::Today)
1872            .build()?;
1873        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1874
1875        Ok(())
1876    }
1877
1878    #[traced_test]
1879    #[test]
1880    fn test_list_issues_created_on_filter_yesterday() -> Result<(), Box<dyn Error>> {
1881        let _r_issues = ISSUES_LOCK.blocking_read();
1882        dotenvy::dotenv()?;
1883        let redmine = crate::api::Redmine::from_env(
1884            reqwest::blocking::Client::builder()
1885                .use_rustls_tls()
1886                .build()?,
1887        )?;
1888
1889        let endpoint = ListIssues::builder()
1890            .created_on(DateTimeFilterPast::Yesterday)
1891            .build()?;
1892        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1893
1894        Ok(())
1895    }
1896
1897    #[traced_test]
1898    #[test]
1899    fn test_list_issues_created_on_filter_this_week() -> Result<(), Box<dyn Error>> {
1900        let _r_issues = ISSUES_LOCK.blocking_read();
1901        dotenvy::dotenv()?;
1902        let redmine = crate::api::Redmine::from_env(
1903            reqwest::blocking::Client::builder()
1904                .use_rustls_tls()
1905                .build()?,
1906        )?;
1907
1908        let endpoint = ListIssues::builder()
1909            .created_on(DateTimeFilterPast::ThisWeek)
1910            .build()?;
1911        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1912
1913        Ok(())
1914    }
1915
1916    #[traced_test]
1917    #[test]
1918    fn test_list_issues_created_on_filter_last_week() -> Result<(), Box<dyn Error>> {
1919        let _r_issues = ISSUES_LOCK.blocking_read();
1920        dotenvy::dotenv()?;
1921        let redmine = crate::api::Redmine::from_env(
1922            reqwest::blocking::Client::builder()
1923                .use_rustls_tls()
1924                .build()?,
1925        )?;
1926
1927        let endpoint = ListIssues::builder()
1928            .created_on(DateTimeFilterPast::LastWeek)
1929            .build()?;
1930        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1931
1932        Ok(())
1933    }
1934
1935    #[traced_test]
1936    #[test]
1937    fn test_list_issues_created_on_filter_last_two_weeks() -> Result<(), Box<dyn Error>> {
1938        let _r_issues = ISSUES_LOCK.blocking_read();
1939        dotenvy::dotenv()?;
1940        let redmine = crate::api::Redmine::from_env(
1941            reqwest::blocking::Client::builder()
1942                .use_rustls_tls()
1943                .build()?,
1944        )?;
1945
1946        let endpoint = ListIssues::builder()
1947            .created_on(DateTimeFilterPast::LastTwoWeeks)
1948            .build()?;
1949        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1950
1951        Ok(())
1952    }
1953
1954    #[traced_test]
1955    #[test]
1956    fn test_list_issues_created_on_filter_this_month() -> Result<(), Box<dyn Error>> {
1957        let _r_issues = ISSUES_LOCK.blocking_read();
1958        dotenvy::dotenv()?;
1959        let redmine = crate::api::Redmine::from_env(
1960            reqwest::blocking::Client::builder()
1961                .use_rustls_tls()
1962                .build()?,
1963        )?;
1964
1965        let endpoint = ListIssues::builder()
1966            .created_on(DateTimeFilterPast::ThisMonth)
1967            .build()?;
1968        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1969
1970        Ok(())
1971    }
1972
1973    #[traced_test]
1974    #[test]
1975    fn test_list_issues_created_on_filter_last_month() -> Result<(), Box<dyn Error>> {
1976        let _r_issues = ISSUES_LOCK.blocking_read();
1977        dotenvy::dotenv()?;
1978        let redmine = crate::api::Redmine::from_env(
1979            reqwest::blocking::Client::builder()
1980                .use_rustls_tls()
1981                .build()?,
1982        )?;
1983
1984        let endpoint = ListIssues::builder()
1985            .created_on(DateTimeFilterPast::LastMonth)
1986            .build()?;
1987        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1988
1989        Ok(())
1990    }
1991
1992    #[traced_test]
1993    #[test]
1994    fn test_list_issues_created_on_filter_this_year() -> Result<(), Box<dyn Error>> {
1995        let _r_issues = ISSUES_LOCK.blocking_read();
1996        dotenvy::dotenv()?;
1997        let redmine = crate::api::Redmine::from_env(
1998            reqwest::blocking::Client::builder()
1999                .use_rustls_tls()
2000                .build()?,
2001        )?;
2002
2003        let endpoint = ListIssues::builder()
2004            .created_on(DateTimeFilterPast::ThisYear)
2005            .build()?;
2006        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2007
2008        Ok(())
2009    }
2010
2011    #[traced_test]
2012    #[test]
2013    fn test_list_issues_created_on_filter_unset() -> Result<(), Box<dyn Error>> {
2014        let _r_issues = ISSUES_LOCK.blocking_read();
2015        dotenvy::dotenv()?;
2016        let redmine = crate::api::Redmine::from_env(
2017            reqwest::blocking::Client::builder()
2018                .use_rustls_tls()
2019                .build()?,
2020        )?;
2021
2022        let endpoint = ListIssues::builder()
2023            .created_on(DateTimeFilterPast::Unset)
2024            .build()?;
2025        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2026
2027        Ok(())
2028    }
2029
2030    #[traced_test]
2031    #[test]
2032    fn test_list_issues_created_on_filter_any() -> Result<(), Box<dyn Error>> {
2033        let _r_issues = ISSUES_LOCK.blocking_read();
2034        dotenvy::dotenv()?;
2035        let redmine = crate::api::Redmine::from_env(
2036            reqwest::blocking::Client::builder()
2037                .use_rustls_tls()
2038                .build()?,
2039        )?;
2040
2041        let endpoint = ListIssues::builder()
2042            .created_on(DateTimeFilterPast::Any)
2043            .build()?;
2044        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2045
2046        Ok(())
2047    }
2048
2049    #[traced_test]
2050    #[test]
2051    fn test_list_issues_updated_on_filter_exact_match() -> Result<(), Box<dyn Error>> {
2052        let _r_issues = ISSUES_LOCK.blocking_read();
2053        dotenvy::dotenv()?;
2054        let redmine = crate::api::Redmine::from_env(
2055            reqwest::blocking::Client::builder()
2056                .use_rustls_tls()
2057                .build()?,
2058        )?;
2059
2060        let dt = time::macros::datetime!(2023-01-15 10:30:00 UTC);
2061        let endpoint = ListIssues::builder()
2062            .updated_on(DateTimeFilterPast::ExactMatch(dt))
2063            .build()?;
2064        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2065
2066        Ok(())
2067    }
2068
2069    #[traced_test]
2070    #[test]
2071    fn test_list_issues_updated_on_filter_range() -> Result<(), Box<dyn Error>> {
2072        let _r_issues = ISSUES_LOCK.blocking_read();
2073        dotenvy::dotenv()?;
2074        let redmine = crate::api::Redmine::from_env(
2075            reqwest::blocking::Client::builder()
2076                .use_rustls_tls()
2077                .build()?,
2078        )?;
2079
2080        let dt_start = time::macros::datetime!(2023-01-01 00:00:00 UTC);
2081        let dt_end = time::macros::datetime!(2023-01-31 23:59:59 UTC);
2082        let endpoint = ListIssues::builder()
2083            .updated_on(DateTimeFilterPast::Range(dt_start, dt_end))
2084            .build()?;
2085        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2086
2087        Ok(())
2088    }
2089
2090    #[traced_test]
2091    #[test]
2092    fn test_list_issues_updated_on_filter_less_than_or_equal() -> Result<(), Box<dyn Error>> {
2093        let _r_issues = ISSUES_LOCK.blocking_read();
2094        dotenvy::dotenv()?;
2095        let redmine = crate::api::Redmine::from_env(
2096            reqwest::blocking::Client::builder()
2097                .use_rustls_tls()
2098                .build()?,
2099        )?;
2100
2101        let dt = time::macros::datetime!(2023-01-15 10:30:00 UTC);
2102        let endpoint = ListIssues::builder()
2103            .updated_on(DateTimeFilterPast::LessThanOrEqual(dt))
2104            .build()?;
2105        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2106
2107        Ok(())
2108    }
2109
2110    #[traced_test]
2111    #[test]
2112    fn test_list_issues_updated_on_filter_greater_than_or_equal() -> Result<(), Box<dyn Error>> {
2113        let _r_issues = ISSUES_LOCK.blocking_read();
2114        dotenvy::dotenv()?;
2115        let redmine = crate::api::Redmine::from_env(
2116            reqwest::blocking::Client::builder()
2117                .use_rustls_tls()
2118                .build()?,
2119        )?;
2120
2121        let dt = time::macros::datetime!(2023-01-15 10:30:00 UTC);
2122        let endpoint = ListIssues::builder()
2123            .updated_on(DateTimeFilterPast::GreaterThanOrEqual(dt))
2124            .build()?;
2125        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2126
2127        Ok(())
2128    }
2129
2130    #[traced_test]
2131    #[test]
2132    fn test_list_issues_updated_on_filter_less_than_days_ago() -> Result<(), Box<dyn Error>> {
2133        let _r_issues = ISSUES_LOCK.blocking_read();
2134        dotenvy::dotenv()?;
2135        let redmine = crate::api::Redmine::from_env(
2136            reqwest::blocking::Client::builder()
2137                .use_rustls_tls()
2138                .build()?,
2139        )?;
2140
2141        let endpoint = ListIssues::builder()
2142            .updated_on(DateTimeFilterPast::LessThanDaysAgo(5))
2143            .build()?;
2144        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2145
2146        Ok(())
2147    }
2148
2149    #[traced_test]
2150    #[test]
2151    fn test_list_issues_updated_on_filter_more_than_days_ago() -> Result<(), Box<dyn Error>> {
2152        let _r_issues = ISSUES_LOCK.blocking_read();
2153        dotenvy::dotenv()?;
2154        let redmine = crate::api::Redmine::from_env(
2155            reqwest::blocking::Client::builder()
2156                .use_rustls_tls()
2157                .build()?,
2158        )?;
2159
2160        let endpoint = ListIssues::builder()
2161            .updated_on(DateTimeFilterPast::MoreThanDaysAgo(10))
2162            .build()?;
2163        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2164
2165        Ok(())
2166    }
2167
2168    #[traced_test]
2169    #[test]
2170    fn test_list_issues_updated_on_filter_within_past_days() -> Result<(), Box<dyn Error>> {
2171        let _r_issues = ISSUES_LOCK.blocking_read();
2172        dotenvy::dotenv()?;
2173        let redmine = crate::api::Redmine::from_env(
2174            reqwest::blocking::Client::builder()
2175                .use_rustls_tls()
2176                .build()?,
2177        )?;
2178
2179        let endpoint = ListIssues::builder()
2180            .updated_on(DateTimeFilterPast::WithinPastDays(7))
2181            .build()?;
2182        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2183
2184        Ok(())
2185    }
2186
2187    #[traced_test]
2188    #[test]
2189    fn test_list_issues_updated_on_filter_exact_days_ago() -> Result<(), Box<dyn Error>> {
2190        let _r_issues = ISSUES_LOCK.blocking_read();
2191        dotenvy::dotenv()?;
2192        let redmine = crate::api::Redmine::from_env(
2193            reqwest::blocking::Client::builder()
2194                .use_rustls_tls()
2195                .build()?,
2196        )?;
2197
2198        let endpoint = ListIssues::builder()
2199            .updated_on(DateTimeFilterPast::ExactDaysAgo(3))
2200            .build()?;
2201        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2202
2203        Ok(())
2204    }
2205
2206    #[traced_test]
2207    #[test]
2208    fn test_list_issues_updated_on_filter_today() -> Result<(), Box<dyn Error>> {
2209        let _r_issues = ISSUES_LOCK.blocking_read();
2210        dotenvy::dotenv()?;
2211        let redmine = crate::api::Redmine::from_env(
2212            reqwest::blocking::Client::builder()
2213                .use_rustls_tls()
2214                .build()?,
2215        )?;
2216
2217        let endpoint = ListIssues::builder()
2218            .updated_on(DateTimeFilterPast::Today)
2219            .build()?;
2220        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2221
2222        Ok(())
2223    }
2224
2225    #[traced_test]
2226    #[test]
2227    fn test_list_issues_updated_on_filter_yesterday() -> Result<(), Box<dyn Error>> {
2228        let _r_issues = ISSUES_LOCK.blocking_read();
2229        dotenvy::dotenv()?;
2230        let redmine = crate::api::Redmine::from_env(
2231            reqwest::blocking::Client::builder()
2232                .use_rustls_tls()
2233                .build()?,
2234        )?;
2235
2236        let endpoint = ListIssues::builder()
2237            .updated_on(DateTimeFilterPast::Yesterday)
2238            .build()?;
2239        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2240
2241        Ok(())
2242    }
2243
2244    #[traced_test]
2245    #[test]
2246    fn test_list_issues_updated_on_filter_this_week() -> Result<(), Box<dyn Error>> {
2247        let _r_issues = ISSUES_LOCK.blocking_read();
2248        dotenvy::dotenv()?;
2249        let redmine = crate::api::Redmine::from_env(
2250            reqwest::blocking::Client::builder()
2251                .use_rustls_tls()
2252                .build()?,
2253        )?;
2254
2255        let endpoint = ListIssues::builder()
2256            .updated_on(DateTimeFilterPast::ThisWeek)
2257            .build()?;
2258        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2259
2260        Ok(())
2261    }
2262
2263    #[traced_test]
2264    #[test]
2265    fn test_list_issues_updated_on_filter_last_week() -> Result<(), Box<dyn Error>> {
2266        let _r_issues = ISSUES_LOCK.blocking_read();
2267        dotenvy::dotenv()?;
2268        let redmine = crate::api::Redmine::from_env(
2269            reqwest::blocking::Client::builder()
2270                .use_rustls_tls()
2271                .build()?,
2272        )?;
2273
2274        let endpoint = ListIssues::builder()
2275            .updated_on(DateTimeFilterPast::LastWeek)
2276            .build()?;
2277        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2278
2279        Ok(())
2280    }
2281
2282    #[traced_test]
2283    #[test]
2284    fn test_list_issues_updated_on_filter_last_two_weeks() -> Result<(), Box<dyn Error>> {
2285        let _r_issues = ISSUES_LOCK.blocking_read();
2286        dotenvy::dotenv()?;
2287        let redmine = crate::api::Redmine::from_env(
2288            reqwest::blocking::Client::builder()
2289                .use_rustls_tls()
2290                .build()?,
2291        )?;
2292
2293        let endpoint = ListIssues::builder()
2294            .updated_on(DateTimeFilterPast::LastTwoWeeks)
2295            .build()?;
2296        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2297
2298        Ok(())
2299    }
2300
2301    #[traced_test]
2302    #[test]
2303    fn test_list_issues_updated_on_filter_this_month() -> Result<(), Box<dyn Error>> {
2304        let _r_issues = ISSUES_LOCK.blocking_read();
2305        dotenvy::dotenv()?;
2306        let redmine = crate::api::Redmine::from_env(
2307            reqwest::blocking::Client::builder()
2308                .use_rustls_tls()
2309                .build()?,
2310        )?;
2311
2312        let endpoint = ListIssues::builder()
2313            .updated_on(DateTimeFilterPast::ThisMonth)
2314            .build()?;
2315        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2316
2317        Ok(())
2318    }
2319
2320    #[traced_test]
2321    #[test]
2322    fn test_list_issues_updated_on_filter_last_month() -> Result<(), Box<dyn Error>> {
2323        let _r_issues = ISSUES_LOCK.blocking_read();
2324        dotenvy::dotenv()?;
2325        let redmine = crate::api::Redmine::from_env(
2326            reqwest::blocking::Client::builder()
2327                .use_rustls_tls()
2328                .build()?,
2329        )?;
2330
2331        let endpoint = ListIssues::builder()
2332            .updated_on(DateTimeFilterPast::LastMonth)
2333            .build()?;
2334        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2335
2336        Ok(())
2337    }
2338
2339    #[traced_test]
2340    #[test]
2341    fn test_list_issues_updated_on_filter_this_year() -> Result<(), Box<dyn Error>> {
2342        let _r_issues = ISSUES_LOCK.blocking_read();
2343        dotenvy::dotenv()?;
2344        let redmine = crate::api::Redmine::from_env(
2345            reqwest::blocking::Client::builder()
2346                .use_rustls_tls()
2347                .build()?,
2348        )?;
2349
2350        let endpoint = ListIssues::builder()
2351            .updated_on(DateTimeFilterPast::ThisYear)
2352            .build()?;
2353        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2354
2355        Ok(())
2356    }
2357
2358    #[traced_test]
2359    #[test]
2360    fn test_list_issues_updated_on_filter_unset() -> Result<(), Box<dyn Error>> {
2361        let _r_issues = ISSUES_LOCK.blocking_read();
2362        dotenvy::dotenv()?;
2363        let redmine = crate::api::Redmine::from_env(
2364            reqwest::blocking::Client::builder()
2365                .use_rustls_tls()
2366                .build()?,
2367        )?;
2368
2369        let endpoint = ListIssues::builder()
2370            .updated_on(DateTimeFilterPast::Unset)
2371            .build()?;
2372        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2373
2374        Ok(())
2375    }
2376
2377    #[traced_test]
2378    #[test]
2379    fn test_list_issues_updated_on_filter_any() -> Result<(), Box<dyn Error>> {
2380        let _r_issues = ISSUES_LOCK.blocking_read();
2381        dotenvy::dotenv()?;
2382        let redmine = crate::api::Redmine::from_env(
2383            reqwest::blocking::Client::builder()
2384                .use_rustls_tls()
2385                .build()?,
2386        )?;
2387
2388        let endpoint = ListIssues::builder()
2389            .updated_on(DateTimeFilterPast::Any)
2390            .build()?;
2391        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2392
2393        Ok(())
2394    }
2395
2396    #[traced_test]
2397    #[test]
2398    fn test_list_issues_start_date_filter_exact_match() -> Result<(), Box<dyn Error>> {
2399        let _r_issues = ISSUES_LOCK.blocking_read();
2400        dotenvy::dotenv()?;
2401        let redmine = crate::api::Redmine::from_env(
2402            reqwest::blocking::Client::builder()
2403                .use_rustls_tls()
2404                .build()?,
2405        )?;
2406
2407        let dt = time::macros::date!(2023 - 01 - 15);
2408        let endpoint = ListIssues::builder()
2409            .start_date(DateFilter::ExactMatch(dt))
2410            .build()?;
2411        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2412
2413        Ok(())
2414    }
2415
2416    #[traced_test]
2417    #[test]
2418    fn test_list_issues_start_date_filter_range() -> Result<(), Box<dyn Error>> {
2419        let _r_issues = ISSUES_LOCK.blocking_read();
2420        dotenvy::dotenv()?;
2421        let redmine = crate::api::Redmine::from_env(
2422            reqwest::blocking::Client::builder()
2423                .use_rustls_tls()
2424                .build()?,
2425        )?;
2426
2427        let dt_start = time::macros::date!(2023 - 01 - 01);
2428        let dt_end = time::macros::date!(2023 - 01 - 31);
2429        let endpoint = ListIssues::builder()
2430            .start_date(DateFilter::Range(dt_start, dt_end))
2431            .build()?;
2432        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2433
2434        Ok(())
2435    }
2436
2437    #[traced_test]
2438    #[test]
2439    fn test_list_issues_start_date_filter_less_than_or_equal() -> Result<(), Box<dyn Error>> {
2440        let _r_issues = ISSUES_LOCK.blocking_read();
2441        dotenvy::dotenv()?;
2442        let redmine = crate::api::Redmine::from_env(
2443            reqwest::blocking::Client::builder()
2444                .use_rustls_tls()
2445                .build()?,
2446        )?;
2447
2448        let dt = time::macros::date!(2023 - 01 - 15);
2449        let endpoint = ListIssues::builder()
2450            .start_date(DateFilter::LessThanOrEqual(dt))
2451            .build()?;
2452        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2453
2454        Ok(())
2455    }
2456
2457    #[traced_test]
2458    #[test]
2459    fn test_list_issues_start_date_filter_greater_than_or_equal() -> Result<(), Box<dyn Error>> {
2460        let _r_issues = ISSUES_LOCK.blocking_read();
2461        dotenvy::dotenv()?;
2462        let redmine = crate::api::Redmine::from_env(
2463            reqwest::blocking::Client::builder()
2464                .use_rustls_tls()
2465                .build()?,
2466        )?;
2467
2468        let dt = time::macros::date!(2023 - 01 - 15);
2469        let endpoint = ListIssues::builder()
2470            .start_date(DateFilter::GreaterThanOrEqual(dt))
2471            .build()?;
2472        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2473
2474        Ok(())
2475    }
2476
2477    #[traced_test]
2478    #[test]
2479    fn test_list_issues_start_date_filter_less_than_days_ago() -> Result<(), Box<dyn Error>> {
2480        let _r_issues = ISSUES_LOCK.blocking_read();
2481        dotenvy::dotenv()?;
2482        let redmine = crate::api::Redmine::from_env(
2483            reqwest::blocking::Client::builder()
2484                .use_rustls_tls()
2485                .build()?,
2486        )?;
2487
2488        let endpoint = ListIssues::builder()
2489            .start_date(DateFilter::LessThanDaysAgo(5))
2490            .build()?;
2491        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2492
2493        Ok(())
2494    }
2495
2496    #[traced_test]
2497    #[test]
2498    fn test_list_issues_start_date_filter_more_than_days_ago() -> Result<(), Box<dyn Error>> {
2499        let _r_issues = ISSUES_LOCK.blocking_read();
2500        dotenvy::dotenv()?;
2501        let redmine = crate::api::Redmine::from_env(
2502            reqwest::blocking::Client::builder()
2503                .use_rustls_tls()
2504                .build()?,
2505        )?;
2506
2507        let endpoint = ListIssues::builder()
2508            .start_date(DateFilter::MoreThanDaysAgo(10))
2509            .build()?;
2510        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2511
2512        Ok(())
2513    }
2514
2515    #[traced_test]
2516    #[test]
2517    fn test_list_issues_start_date_filter_within_past_days() -> Result<(), Box<dyn Error>> {
2518        let _r_issues = ISSUES_LOCK.blocking_read();
2519        dotenvy::dotenv()?;
2520        let redmine = crate::api::Redmine::from_env(
2521            reqwest::blocking::Client::builder()
2522                .use_rustls_tls()
2523                .build()?,
2524        )?;
2525
2526        let endpoint = ListIssues::builder()
2527            .start_date(DateFilter::WithinPastDays(7))
2528            .build()?;
2529        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2530
2531        Ok(())
2532    }
2533
2534    #[traced_test]
2535    #[test]
2536    fn test_list_issues_start_date_filter_exact_days_ago() -> Result<(), Box<dyn Error>> {
2537        let _r_issues = ISSUES_LOCK.blocking_read();
2538        dotenvy::dotenv()?;
2539        let redmine = crate::api::Redmine::from_env(
2540            reqwest::blocking::Client::builder()
2541                .use_rustls_tls()
2542                .build()?,
2543        )?;
2544
2545        let endpoint = ListIssues::builder()
2546            .start_date(DateFilter::ExactDaysAgo(3))
2547            .build()?;
2548        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2549
2550        Ok(())
2551    }
2552
2553    #[traced_test]
2554    #[test]
2555    fn test_list_issues_start_date_filter_in_less_than_days() -> Result<(), Box<dyn Error>> {
2556        let _r_issues = ISSUES_LOCK.blocking_read();
2557        dotenvy::dotenv()?;
2558        let redmine = crate::api::Redmine::from_env(
2559            reqwest::blocking::Client::builder()
2560                .use_rustls_tls()
2561                .build()?,
2562        )?;
2563
2564        let endpoint = ListIssues::builder()
2565            .start_date(DateFilter::InLessThanDays(5))
2566            .build()?;
2567        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2568
2569        Ok(())
2570    }
2571
2572    #[traced_test]
2573    #[test]
2574    fn test_list_issues_start_date_filter_in_more_than_days() -> Result<(), Box<dyn Error>> {
2575        let _r_issues = ISSUES_LOCK.blocking_read();
2576        dotenvy::dotenv()?;
2577        let redmine = crate::api::Redmine::from_env(
2578            reqwest::blocking::Client::builder()
2579                .use_rustls_tls()
2580                .build()?,
2581        )?;
2582
2583        let endpoint = ListIssues::builder()
2584            .start_date(DateFilter::InMoreThanDays(10))
2585            .build()?;
2586        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2587
2588        Ok(())
2589    }
2590
2591    #[traced_test]
2592    #[test]
2593    fn test_list_issues_start_date_filter_within_future_days() -> Result<(), Box<dyn Error>> {
2594        let _r_issues = ISSUES_LOCK.blocking_read();
2595        dotenvy::dotenv()?;
2596        let redmine = crate::api::Redmine::from_env(
2597            reqwest::blocking::Client::builder()
2598                .use_rustls_tls()
2599                .build()?,
2600        )?;
2601
2602        let endpoint = ListIssues::builder()
2603            .start_date(DateFilter::WithinFutureDays(7))
2604            .build()?;
2605        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2606
2607        Ok(())
2608    }
2609
2610    #[traced_test]
2611    #[test]
2612    fn test_list_issues_start_date_filter_in_exact_days() -> Result<(), Box<dyn Error>> {
2613        let _r_issues = ISSUES_LOCK.blocking_read();
2614        dotenvy::dotenv()?;
2615        let redmine = crate::api::Redmine::from_env(
2616            reqwest::blocking::Client::builder()
2617                .use_rustls_tls()
2618                .build()?,
2619        )?;
2620
2621        let endpoint = ListIssues::builder()
2622            .start_date(DateFilter::InExactDays(3))
2623            .build()?;
2624        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2625
2626        Ok(())
2627    }
2628
2629    #[traced_test]
2630    #[test]
2631    fn test_list_issues_start_date_filter_today() -> Result<(), Box<dyn Error>> {
2632        let _r_issues = ISSUES_LOCK.blocking_read();
2633        dotenvy::dotenv()?;
2634        let redmine = crate::api::Redmine::from_env(
2635            reqwest::blocking::Client::builder()
2636                .use_rustls_tls()
2637                .build()?,
2638        )?;
2639
2640        let endpoint = ListIssues::builder()
2641            .start_date(DateFilter::Today)
2642            .build()?;
2643        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2644
2645        Ok(())
2646    }
2647
2648    #[traced_test]
2649    #[test]
2650    fn test_list_issues_start_date_filter_yesterday() -> Result<(), Box<dyn Error>> {
2651        let _r_issues = ISSUES_LOCK.blocking_read();
2652        dotenvy::dotenv()?;
2653        let redmine = crate::api::Redmine::from_env(
2654            reqwest::blocking::Client::builder()
2655                .use_rustls_tls()
2656                .build()?,
2657        )?;
2658
2659        let endpoint = ListIssues::builder()
2660            .start_date(DateFilter::Yesterday)
2661            .build()?;
2662        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2663
2664        Ok(())
2665    }
2666
2667    #[traced_test]
2668    #[test]
2669    fn test_list_issues_start_date_filter_tomorrow() -> Result<(), Box<dyn Error>> {
2670        let _r_issues = ISSUES_LOCK.blocking_read();
2671        dotenvy::dotenv()?;
2672        let redmine = crate::api::Redmine::from_env(
2673            reqwest::blocking::Client::builder()
2674                .use_rustls_tls()
2675                .build()?,
2676        )?;
2677
2678        let endpoint = ListIssues::builder()
2679            .start_date(DateFilter::Tomorrow)
2680            .build()?;
2681        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2682
2683        Ok(())
2684    }
2685
2686    #[traced_test]
2687    #[test]
2688    fn test_list_issues_start_date_filter_this_week() -> Result<(), Box<dyn Error>> {
2689        let _r_issues = ISSUES_LOCK.blocking_read();
2690        dotenvy::dotenv()?;
2691        let redmine = crate::api::Redmine::from_env(
2692            reqwest::blocking::Client::builder()
2693                .use_rustls_tls()
2694                .build()?,
2695        )?;
2696
2697        let endpoint = ListIssues::builder()
2698            .start_date(DateFilter::ThisWeek)
2699            .build()?;
2700        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2701
2702        Ok(())
2703    }
2704
2705    #[traced_test]
2706    #[test]
2707    fn test_list_issues_start_date_filter_last_week() -> Result<(), Box<dyn Error>> {
2708        let _r_issues = ISSUES_LOCK.blocking_read();
2709        dotenvy::dotenv()?;
2710        let redmine = crate::api::Redmine::from_env(
2711            reqwest::blocking::Client::builder()
2712                .use_rustls_tls()
2713                .build()?,
2714        )?;
2715
2716        let endpoint = ListIssues::builder()
2717            .start_date(DateFilter::LastWeek)
2718            .build()?;
2719        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2720
2721        Ok(())
2722    }
2723
2724    #[traced_test]
2725    #[test]
2726    fn test_list_issues_start_date_filter_last_two_weeks() -> Result<(), Box<dyn Error>> {
2727        let _r_issues = ISSUES_LOCK.blocking_read();
2728        dotenvy::dotenv()?;
2729        let redmine = crate::api::Redmine::from_env(
2730            reqwest::blocking::Client::builder()
2731                .use_rustls_tls()
2732                .build()?,
2733        )?;
2734
2735        let endpoint = ListIssues::builder()
2736            .start_date(DateFilter::LastTwoWeeks)
2737            .build()?;
2738        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2739
2740        Ok(())
2741    }
2742
2743    #[traced_test]
2744    #[test]
2745    fn test_list_issues_start_date_filter_next_week() -> Result<(), Box<dyn Error>> {
2746        let _r_issues = ISSUES_LOCK.blocking_read();
2747        dotenvy::dotenv()?;
2748        let redmine = crate::api::Redmine::from_env(
2749            reqwest::blocking::Client::builder()
2750                .use_rustls_tls()
2751                .build()?,
2752        )?;
2753
2754        let endpoint = ListIssues::builder()
2755            .start_date(DateFilter::NextWeek)
2756            .build()?;
2757        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2758
2759        Ok(())
2760    }
2761
2762    #[traced_test]
2763    #[test]
2764    fn test_list_issues_start_date_filter_this_month() -> Result<(), Box<dyn Error>> {
2765        let _r_issues = ISSUES_LOCK.blocking_read();
2766        dotenvy::dotenv()?;
2767        let redmine = crate::api::Redmine::from_env(
2768            reqwest::blocking::Client::builder()
2769                .use_rustls_tls()
2770                .build()?,
2771        )?;
2772
2773        let endpoint = ListIssues::builder()
2774            .start_date(DateFilter::ThisMonth)
2775            .build()?;
2776        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2777
2778        Ok(())
2779    }
2780
2781    #[traced_test]
2782    #[test]
2783    fn test_list_issues_start_date_filter_last_month() -> Result<(), Box<dyn Error>> {
2784        let _r_issues = ISSUES_LOCK.blocking_read();
2785        dotenvy::dotenv()?;
2786        let redmine = crate::api::Redmine::from_env(
2787            reqwest::blocking::Client::builder()
2788                .use_rustls_tls()
2789                .build()?,
2790        )?;
2791
2792        let endpoint = ListIssues::builder()
2793            .start_date(DateFilter::LastMonth)
2794            .build()?;
2795        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2796
2797        Ok(())
2798    }
2799
2800    #[traced_test]
2801    #[test]
2802    fn test_list_issues_start_date_filter_next_month() -> Result<(), Box<dyn Error>> {
2803        let _r_issues = ISSUES_LOCK.blocking_read();
2804        dotenvy::dotenv()?;
2805        let redmine = crate::api::Redmine::from_env(
2806            reqwest::blocking::Client::builder()
2807                .use_rustls_tls()
2808                .build()?,
2809        )?;
2810
2811        let endpoint = ListIssues::builder()
2812            .start_date(DateFilter::NextMonth)
2813            .build()?;
2814        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2815
2816        Ok(())
2817    }
2818
2819    #[traced_test]
2820    #[test]
2821    fn test_list_issues_start_date_filter_this_year() -> Result<(), Box<dyn Error>> {
2822        let _r_issues = ISSUES_LOCK.blocking_read();
2823        dotenvy::dotenv()?;
2824        let redmine = crate::api::Redmine::from_env(
2825            reqwest::blocking::Client::builder()
2826                .use_rustls_tls()
2827                .build()?,
2828        )?;
2829
2830        let endpoint = ListIssues::builder()
2831            .start_date(DateFilter::ThisYear)
2832            .build()?;
2833        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2834
2835        Ok(())
2836    }
2837
2838    #[traced_test]
2839    #[test]
2840    fn test_list_issues_start_date_filter_unset() -> Result<(), Box<dyn Error>> {
2841        let _r_issues = ISSUES_LOCK.blocking_read();
2842        dotenvy::dotenv()?;
2843        let redmine = crate::api::Redmine::from_env(
2844            reqwest::blocking::Client::builder()
2845                .use_rustls_tls()
2846                .build()?,
2847        )?;
2848
2849        let endpoint = ListIssues::builder()
2850            .start_date(DateFilter::Unset)
2851            .build()?;
2852        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2853
2854        Ok(())
2855    }
2856
2857    #[traced_test]
2858    #[test]
2859    fn test_list_issues_start_date_filter_any() -> Result<(), Box<dyn Error>> {
2860        let _r_issues = ISSUES_LOCK.blocking_read();
2861        dotenvy::dotenv()?;
2862        let redmine = crate::api::Redmine::from_env(
2863            reqwest::blocking::Client::builder()
2864                .use_rustls_tls()
2865                .build()?,
2866        )?;
2867
2868        let endpoint = ListIssues::builder().start_date(DateFilter::Any).build()?;
2869        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2870
2871        Ok(())
2872    }
2873
2874    #[traced_test]
2875    #[test]
2876    fn test_list_issues_due_date_filter_exact_match() -> Result<(), Box<dyn Error>> {
2877        let _r_issues = ISSUES_LOCK.blocking_read();
2878        dotenvy::dotenv()?;
2879        let redmine = crate::api::Redmine::from_env(
2880            reqwest::blocking::Client::builder()
2881                .use_rustls_tls()
2882                .build()?,
2883        )?;
2884
2885        let dt = time::macros::date!(2023 - 01 - 15);
2886        let endpoint = ListIssues::builder()
2887            .due_date(DateFilter::ExactMatch(dt))
2888            .build()?;
2889        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2890
2891        Ok(())
2892    }
2893
2894    #[traced_test]
2895    #[test]
2896    fn test_list_issues_due_date_filter_range() -> Result<(), Box<dyn Error>> {
2897        let _r_issues = ISSUES_LOCK.blocking_read();
2898        dotenvy::dotenv()?;
2899        let redmine = crate::api::Redmine::from_env(
2900            reqwest::blocking::Client::builder()
2901                .use_rustls_tls()
2902                .build()?,
2903        )?;
2904
2905        let dt_start = time::macros::date!(2023 - 01 - 01);
2906        let dt_end = time::macros::date!(2023 - 01 - 31);
2907        let endpoint = ListIssues::builder()
2908            .due_date(DateFilter::Range(dt_start, dt_end))
2909            .build()?;
2910        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2911
2912        Ok(())
2913    }
2914
2915    #[traced_test]
2916    #[test]
2917    fn test_list_issues_due_date_filter_less_than_or_equal() -> Result<(), Box<dyn Error>> {
2918        let _r_issues = ISSUES_LOCK.blocking_read();
2919        dotenvy::dotenv()?;
2920        let redmine = crate::api::Redmine::from_env(
2921            reqwest::blocking::Client::builder()
2922                .use_rustls_tls()
2923                .build()?,
2924        )?;
2925
2926        let dt = time::macros::date!(2023 - 01 - 15);
2927        let endpoint = ListIssues::builder()
2928            .due_date(DateFilter::LessThanOrEqual(dt))
2929            .build()?;
2930        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2931
2932        Ok(())
2933    }
2934
2935    #[traced_test]
2936    #[test]
2937    fn test_list_issues_due_date_filter_greater_than_or_equal() -> Result<(), Box<dyn Error>> {
2938        let _r_issues = ISSUES_LOCK.blocking_read();
2939        dotenvy::dotenv()?;
2940        let redmine = crate::api::Redmine::from_env(
2941            reqwest::blocking::Client::builder()
2942                .use_rustls_tls()
2943                .build()?,
2944        )?;
2945
2946        let dt = time::macros::date!(2023 - 01 - 15);
2947        let endpoint = ListIssues::builder()
2948            .due_date(DateFilter::GreaterThanOrEqual(dt))
2949            .build()?;
2950        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2951
2952        Ok(())
2953    }
2954
2955    #[traced_test]
2956    #[test]
2957    fn test_list_issues_due_date_filter_less_than_days_ago() -> Result<(), Box<dyn Error>> {
2958        let _r_issues = ISSUES_LOCK.blocking_read();
2959        dotenvy::dotenv()?;
2960        let redmine = crate::api::Redmine::from_env(
2961            reqwest::blocking::Client::builder()
2962                .use_rustls_tls()
2963                .build()?,
2964        )?;
2965
2966        let endpoint = ListIssues::builder()
2967            .due_date(DateFilter::LessThanDaysAgo(5))
2968            .build()?;
2969        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2970
2971        Ok(())
2972    }
2973
2974    #[traced_test]
2975    #[test]
2976    fn test_list_issues_due_date_filter_more_than_days_ago() -> Result<(), Box<dyn Error>> {
2977        let _r_issues = ISSUES_LOCK.blocking_read();
2978        dotenvy::dotenv()?;
2979        let redmine = crate::api::Redmine::from_env(
2980            reqwest::blocking::Client::builder()
2981                .use_rustls_tls()
2982                .build()?,
2983        )?;
2984
2985        let endpoint = ListIssues::builder()
2986            .due_date(DateFilter::MoreThanDaysAgo(10))
2987            .build()?;
2988        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
2989
2990        Ok(())
2991    }
2992
2993    #[traced_test]
2994    #[test]
2995    fn test_list_issues_due_date_filter_within_past_days() -> Result<(), Box<dyn Error>> {
2996        let _r_issues = ISSUES_LOCK.blocking_read();
2997        dotenvy::dotenv()?;
2998        let redmine = crate::api::Redmine::from_env(
2999            reqwest::blocking::Client::builder()
3000                .use_rustls_tls()
3001                .build()?,
3002        )?;
3003
3004        let endpoint = ListIssues::builder()
3005            .due_date(DateFilter::WithinPastDays(7))
3006            .build()?;
3007        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3008
3009        Ok(())
3010    }
3011
3012    #[traced_test]
3013    #[test]
3014    fn test_list_issues_due_date_filter_exact_days_ago() -> Result<(), Box<dyn Error>> {
3015        let _r_issues = ISSUES_LOCK.blocking_read();
3016        dotenvy::dotenv()?;
3017        let redmine = crate::api::Redmine::from_env(
3018            reqwest::blocking::Client::builder()
3019                .use_rustls_tls()
3020                .build()?,
3021        )?;
3022
3023        let endpoint = ListIssues::builder()
3024            .due_date(DateFilter::ExactDaysAgo(3))
3025            .build()?;
3026        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3027
3028        Ok(())
3029    }
3030
3031    #[traced_test]
3032    #[test]
3033    fn test_list_issues_due_date_filter_in_less_than_days() -> Result<(), Box<dyn Error>> {
3034        let _r_issues = ISSUES_LOCK.blocking_read();
3035        dotenvy::dotenv()?;
3036        let redmine = crate::api::Redmine::from_env(
3037            reqwest::blocking::Client::builder()
3038                .use_rustls_tls()
3039                .build()?,
3040        )?;
3041
3042        let endpoint = ListIssues::builder()
3043            .due_date(DateFilter::InLessThanDays(5))
3044            .build()?;
3045        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3046
3047        Ok(())
3048    }
3049
3050    #[traced_test]
3051    #[test]
3052    fn test_list_issues_due_date_filter_in_more_than_days() -> Result<(), Box<dyn Error>> {
3053        let _r_issues = ISSUES_LOCK.blocking_read();
3054        dotenvy::dotenv()?;
3055        let redmine = crate::api::Redmine::from_env(
3056            reqwest::blocking::Client::builder()
3057                .use_rustls_tls()
3058                .build()?,
3059        )?;
3060
3061        let endpoint = ListIssues::builder()
3062            .due_date(DateFilter::InMoreThanDays(10))
3063            .build()?;
3064        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3065
3066        Ok(())
3067    }
3068
3069    #[traced_test]
3070    #[test]
3071    fn test_list_issues_due_date_filter_within_future_days() -> Result<(), Box<dyn Error>> {
3072        let _r_issues = ISSUES_LOCK.blocking_read();
3073        dotenvy::dotenv()?;
3074        let redmine = crate::api::Redmine::from_env(
3075            reqwest::blocking::Client::builder()
3076                .use_rustls_tls()
3077                .build()?,
3078        )?;
3079
3080        let endpoint = ListIssues::builder()
3081            .due_date(DateFilter::WithinFutureDays(7))
3082            .build()?;
3083        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3084
3085        Ok(())
3086    }
3087
3088    #[traced_test]
3089    #[test]
3090    fn test_list_issues_due_date_filter_in_exact_days() -> Result<(), Box<dyn Error>> {
3091        let _r_issues = ISSUES_LOCK.blocking_read();
3092        dotenvy::dotenv()?;
3093        let redmine = crate::api::Redmine::from_env(
3094            reqwest::blocking::Client::builder()
3095                .use_rustls_tls()
3096                .build()?,
3097        )?;
3098
3099        let endpoint = ListIssues::builder()
3100            .due_date(DateFilter::InExactDays(3))
3101            .build()?;
3102        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3103
3104        Ok(())
3105    }
3106
3107    #[traced_test]
3108    #[test]
3109    fn test_list_issues_due_date_filter_today() -> Result<(), Box<dyn Error>> {
3110        let _r_issues = ISSUES_LOCK.blocking_read();
3111        dotenvy::dotenv()?;
3112        let redmine = crate::api::Redmine::from_env(
3113            reqwest::blocking::Client::builder()
3114                .use_rustls_tls()
3115                .build()?,
3116        )?;
3117
3118        let endpoint = ListIssues::builder().due_date(DateFilter::Today).build()?;
3119        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3120
3121        Ok(())
3122    }
3123
3124    #[traced_test]
3125    #[test]
3126    fn test_list_issues_due_date_filter_yesterday() -> Result<(), Box<dyn Error>> {
3127        let _r_issues = ISSUES_LOCK.blocking_read();
3128        dotenvy::dotenv()?;
3129        let redmine = crate::api::Redmine::from_env(
3130            reqwest::blocking::Client::builder()
3131                .use_rustls_tls()
3132                .build()?,
3133        )?;
3134
3135        let endpoint = ListIssues::builder()
3136            .due_date(DateFilter::Yesterday)
3137            .build()?;
3138        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3139
3140        Ok(())
3141    }
3142
3143    #[traced_test]
3144    #[test]
3145    fn test_list_issues_due_date_filter_tomorrow() -> Result<(), Box<dyn Error>> {
3146        let _r_issues = ISSUES_LOCK.blocking_read();
3147        dotenvy::dotenv()?;
3148        let redmine = crate::api::Redmine::from_env(
3149            reqwest::blocking::Client::builder()
3150                .use_rustls_tls()
3151                .build()?,
3152        )?;
3153
3154        let endpoint = ListIssues::builder()
3155            .due_date(DateFilter::Tomorrow)
3156            .build()?;
3157        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3158
3159        Ok(())
3160    }
3161
3162    #[traced_test]
3163    #[test]
3164    fn test_list_issues_due_date_filter_this_week() -> Result<(), Box<dyn Error>> {
3165        let _r_issues = ISSUES_LOCK.blocking_read();
3166        dotenvy::dotenv()?;
3167        let redmine = crate::api::Redmine::from_env(
3168            reqwest::blocking::Client::builder()
3169                .use_rustls_tls()
3170                .build()?,
3171        )?;
3172
3173        let endpoint = ListIssues::builder()
3174            .due_date(DateFilter::ThisWeek)
3175            .build()?;
3176        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3177
3178        Ok(())
3179    }
3180
3181    #[traced_test]
3182    #[test]
3183    fn test_list_issues_due_date_filter_last_week() -> Result<(), Box<dyn Error>> {
3184        let _r_issues = ISSUES_LOCK.blocking_read();
3185        dotenvy::dotenv()?;
3186        let redmine = crate::api::Redmine::from_env(
3187            reqwest::blocking::Client::builder()
3188                .use_rustls_tls()
3189                .build()?,
3190        )?;
3191
3192        let endpoint = ListIssues::builder()
3193            .due_date(DateFilter::LastWeek)
3194            .build()?;
3195        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3196
3197        Ok(())
3198    }
3199
3200    #[traced_test]
3201    #[test]
3202    fn test_list_issues_due_date_filter_last_two_weeks() -> Result<(), Box<dyn Error>> {
3203        let _r_issues = ISSUES_LOCK.blocking_read();
3204        dotenvy::dotenv()?;
3205        let redmine = crate::api::Redmine::from_env(
3206            reqwest::blocking::Client::builder()
3207                .use_rustls_tls()
3208                .build()?,
3209        )?;
3210
3211        let endpoint = ListIssues::builder()
3212            .due_date(DateFilter::LastTwoWeeks)
3213            .build()?;
3214        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3215
3216        Ok(())
3217    }
3218
3219    #[traced_test]
3220    #[test]
3221    fn test_list_issues_due_date_filter_next_week() -> Result<(), Box<dyn Error>> {
3222        let _r_issues = ISSUES_LOCK.blocking_read();
3223        dotenvy::dotenv()?;
3224        let redmine = crate::api::Redmine::from_env(
3225            reqwest::blocking::Client::builder()
3226                .use_rustls_tls()
3227                .build()?,
3228        )?;
3229
3230        let endpoint = ListIssues::builder()
3231            .due_date(DateFilter::NextWeek)
3232            .build()?;
3233        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3234
3235        Ok(())
3236    }
3237
3238    #[traced_test]
3239    #[test]
3240    fn test_list_issues_due_date_filter_this_month() -> Result<(), Box<dyn Error>> {
3241        let _r_issues = ISSUES_LOCK.blocking_read();
3242        dotenvy::dotenv()?;
3243        let redmine = crate::api::Redmine::from_env(
3244            reqwest::blocking::Client::builder()
3245                .use_rustls_tls()
3246                .build()?,
3247        )?;
3248
3249        let endpoint = ListIssues::builder()
3250            .due_date(DateFilter::ThisMonth)
3251            .build()?;
3252        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3253
3254        Ok(())
3255    }
3256
3257    #[traced_test]
3258    #[test]
3259    fn test_list_issues_due_date_filter_last_month() -> Result<(), Box<dyn Error>> {
3260        let _r_issues = ISSUES_LOCK.blocking_read();
3261        dotenvy::dotenv()?;
3262        let redmine = crate::api::Redmine::from_env(
3263            reqwest::blocking::Client::builder()
3264                .use_rustls_tls()
3265                .build()?,
3266        )?;
3267
3268        let endpoint = ListIssues::builder()
3269            .due_date(DateFilter::LastMonth)
3270            .build()?;
3271        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3272
3273        Ok(())
3274    }
3275
3276    #[traced_test]
3277    #[test]
3278    fn test_list_issues_due_date_filter_next_month() -> Result<(), Box<dyn Error>> {
3279        let _r_issues = ISSUES_LOCK.blocking_read();
3280        dotenvy::dotenv()?;
3281        let redmine = crate::api::Redmine::from_env(
3282            reqwest::blocking::Client::builder()
3283                .use_rustls_tls()
3284                .build()?,
3285        )?;
3286
3287        let endpoint = ListIssues::builder()
3288            .due_date(DateFilter::NextMonth)
3289            .build()?;
3290        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3291
3292        Ok(())
3293    }
3294
3295    #[traced_test]
3296    #[test]
3297    fn test_list_issues_due_date_filter_this_year() -> Result<(), Box<dyn Error>> {
3298        let _r_issues = ISSUES_LOCK.blocking_read();
3299        dotenvy::dotenv()?;
3300        let redmine = crate::api::Redmine::from_env(
3301            reqwest::blocking::Client::builder()
3302                .use_rustls_tls()
3303                .build()?,
3304        )?;
3305
3306        let endpoint = ListIssues::builder()
3307            .due_date(DateFilter::ThisYear)
3308            .build()?;
3309        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3310
3311        Ok(())
3312    }
3313
3314    #[traced_test]
3315    #[test]
3316    fn test_list_issues_due_date_filter_unset() -> Result<(), Box<dyn Error>> {
3317        let _r_issues = ISSUES_LOCK.blocking_read();
3318        dotenvy::dotenv()?;
3319        let redmine = crate::api::Redmine::from_env(
3320            reqwest::blocking::Client::builder()
3321                .use_rustls_tls()
3322                .build()?,
3323        )?;
3324
3325        let endpoint = ListIssues::builder().due_date(DateFilter::Unset).build()?;
3326        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3327
3328        Ok(())
3329    }
3330
3331    #[traced_test]
3332    #[test]
3333    fn test_list_issues_due_date_filter_any() -> Result<(), Box<dyn Error>> {
3334        let _r_issues = ISSUES_LOCK.blocking_read();
3335        dotenvy::dotenv()?;
3336        let redmine = crate::api::Redmine::from_env(
3337            reqwest::blocking::Client::builder()
3338                .use_rustls_tls()
3339                .build()?,
3340        )?;
3341
3342        let endpoint = ListIssues::builder().due_date(DateFilter::Any).build()?;
3343        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3344
3345        Ok(())
3346    }
3347
3348    #[traced_test]
3349    #[test]
3350    fn test_list_issues_child_id_filter() -> Result<(), Box<dyn Error>> {
3351        let _r_issues = ISSUES_LOCK.blocking_read();
3352        dotenvy::dotenv()?;
3353        let redmine = crate::api::Redmine::from_env(
3354            reqwest::blocking::Client::builder()
3355                .use_rustls_tls()
3356                .build()?,
3357        )?;
3358
3359        let endpoint = ListIssues::builder().child_id(vec![123, 456]).build()?;
3360        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
3361
3362        Ok(())
3363    }
3364}