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