veracode_platform/
app.rs

1//! Application-specific functionality built on top of the core client.
2//!
3//! This module contains application-specific methods and convenience functions
4//! that use the core VeracodeClient to perform application-related operations.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::{VeracodeError};
10use crate::client::VeracodeClient;
11
12/// Represents a Veracode application.
13///
14/// This struct contains all the information about a Veracode application,
15/// including its profile, scans, and metadata.
16#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct Application {
18    /// Globally unique identifier (GUID) for the application
19    pub guid: String,
20    /// Unique numeric identifier for id the application
21    pub id: u64,
22    /// Organization ID
23    pub oid: Option<u64>,
24    /// Organization ID
25    pub alt_org_id: Option<u64>,
26    /// Unique numeric identifier for organization_id the application
27    pub organization_id: Option<u64>,
28    /// ISO 8601 timestamp of the last completed scan
29    pub created: String,
30    /// ISO 8601 timestamp when the application was last modified
31    pub modified: Option<String>,
32    /// ISO 8601 timestamp of the last completed scan
33    pub last_completed_scan_date: Option<String>,
34    /// ISO 8601 timestamp of the last policy compliance check
35    pub last_policy_compliance_check_date: Option<String>,
36    /// URL to the application profile in the Veracode platform
37    pub app_profile_url: Option<String>,
38    /// Detailed application profile information
39    pub profile: Option<Profile>,
40    /// List of scans associated with this application
41    pub scans: Option<Vec<Scan>>,
42    /// URL to the application profile in the Veracode platform
43    pub results_url: Option<String>,
44}
45
46/// Application profile information.
47#[derive(Debug, Serialize, Deserialize, Clone)]
48pub struct Profile {
49    /// Profile name
50    pub name: String,
51    /// Profile description
52    pub description: Option<String>,
53    /// Profile tags
54    pub tags: Option<String>,
55    /// Business unit associated with the application
56    pub business_unit: Option<BusinessUnit>,
57    /// List of business owners
58    pub business_owners: Option<Vec<BusinessOwner>>,
59    /// List of policies applied to the application
60    pub policies: Option<Vec<Policy>>,
61    /// List of teams associated with the application
62    pub teams: Option<Vec<Team>>,
63    /// Archer application name
64    pub archer_app_name: Option<String>,
65    /// Custom fields
66    pub custom_fields: Option<Vec<CustomField>>,
67    /// Business criticality level (required)
68    #[serde(serialize_with = "serialize_business_criticality")]
69    pub business_criticality: BusinessCriticality,
70    /// Application Profile Settings
71    pub settings: Option<Settings>
72}
73
74#[derive(Debug, Serialize, Deserialize, Clone)]
75pub struct Settings {
76    /// Profile name
77    pub nextday_consultation_allowed: bool,
78    /// Profile description
79    pub static_scan_xpa_or_dpa: bool,
80    /// Profile tags
81    pub dynamic_scan_approval_not_required: bool,
82    /// Business unit associated with the application
83    pub sca_enabled: bool,
84    /// List of business owners
85    pub static_scan_xpp_enabled: bool,
86}
87
88/// Business unit information.
89#[derive(Debug, Serialize, Deserialize, Clone)]
90pub struct BusinessUnit {
91    /// Business unit ID
92    pub id: Option<u64>,
93    /// Business unit name
94    pub name: Option<String>,
95    /// Business unit GUID
96    pub guid: Option<String>,
97}
98
99/// Business owner information.
100#[derive(Debug, Serialize, Deserialize, Clone)]
101pub struct BusinessOwner {
102    /// Owner's email address
103    pub email: Option<String>,
104    /// Owner's name
105    pub name: Option<String>,
106}
107
108/// Policy information.
109#[derive(Debug, Serialize, Deserialize, Clone)]
110pub struct Policy {
111    /// Policy GUID
112    pub guid: String,
113    /// Policy name
114    pub name: String,
115    /// Whether this is the default policy
116    pub is_default: bool,
117    /// Policy compliance status
118    pub policy_compliance_status: Option<String>,
119}
120
121/// Team information.
122#[derive(Debug, Serialize, Deserialize, Clone)]
123pub struct Team {
124    /// Team ID
125    pub team_id: Option<u64>,
126    /// Team name
127    pub team_name: Option<String>,
128    /// Legacy team ID
129    pub team_legacy_id: Option<u64>,
130}
131
132/// Custom field information.
133#[derive(Debug, Serialize, Deserialize, Clone)]
134pub struct CustomField {
135    /// Field name
136    pub name: Option<String>,
137    /// Field value
138    pub value: Option<String>,
139}
140
141/// Scan information.
142#[derive(Debug, Serialize, Deserialize, Clone)]
143pub struct Scan {
144    /// Scan ID
145    pub scan_id: Option<u64>,
146    /// Type of scan (STATIC, DYNAMIC, etc.)
147    pub scan_type: Option<String>,
148    /// Scan status
149    pub status: Option<String>,
150    /// URL to the scan results
151    pub scan_url: Option<String>,
152    /// When the scan was last modified
153    pub modified_date: Option<String>,
154    /// Internal scan status
155    pub internal_status: Option<String>,
156    /// Related links
157    pub links: Option<Vec<Link>>,
158    /// Fallback scan type
159    pub fallback_type: Option<String>,
160    /// Full scan type
161    pub full_type: Option<String>,
162}
163
164/// Link information.
165#[derive(Debug, Serialize, Deserialize, Clone)]
166pub struct Link {
167    /// Link relationship
168    pub rel: Option<String>,
169    /// Link URL
170    pub href: Option<String>,
171}
172
173/// Response from the Applications API.
174#[derive(Debug, Serialize, Deserialize, Clone)]
175pub struct ApplicationsResponse {
176    /// Embedded applications data
177    #[serde(rename = "_embedded")]
178    pub embedded: Option<EmbeddedApplications>,
179    /// Pagination information
180    pub page: Option<PageInfo>,
181    /// Response links
182    #[serde(rename = "_links")]
183    pub links: Option<HashMap<String, Link>>,
184}
185
186/// Embedded applications in the response.
187#[derive(Debug, Serialize, Deserialize, Clone)]
188pub struct EmbeddedApplications {
189    /// List of applications
190    pub applications: Vec<Application>,
191}
192
193/// Pagination information.
194#[derive(Debug, Serialize, Deserialize, Clone)]
195pub struct PageInfo {
196    /// Number of items per page
197    pub size: Option<u32>,
198    /// Current page number
199    pub number: Option<u32>,
200    /// Total number of elements
201    pub total_elements: Option<u64>,
202    /// Total number of pages
203    pub total_pages: Option<u32>,
204}
205
206/// Request for creating a new application.
207#[derive(Debug, Serialize, Deserialize, Clone)]
208pub struct CreateApplicationRequest {
209    /// Application profile information
210    pub profile: CreateApplicationProfile,
211}
212
213/// Profile information for creating an application.
214#[derive(Debug, Serialize, Deserialize, Clone)]
215pub struct CreateApplicationProfile {
216    /// Application name
217    pub name: String,
218    /// Business criticality level (required)
219    #[serde(serialize_with = "serialize_business_criticality")]
220    pub business_criticality: BusinessCriticality,
221    /// Application description
222    pub description: Option<String>,
223    /// Business unit
224    pub business_unit: Option<BusinessUnit>,
225    /// Business owners
226    pub business_owners: Option<Vec<BusinessOwner>>,
227    /// Policies
228    pub policies: Option<Vec<Policy>>,
229    /// Teams
230    pub teams: Option<Vec<Team>>,
231    /// Tags
232    pub tags: Option<String>,
233    /// Custom fields
234    pub custom_fields: Option<Vec<CustomField>>,
235}
236
237/// Business criticality levels for applications
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum BusinessCriticality {
240    VeryHigh,
241    High,
242    Medium,
243    Low,
244    VeryLow,
245}
246
247impl BusinessCriticality {
248    /// Convert to the string value expected by the API
249    pub fn as_str(&self) -> &'static str {
250        match self {
251            BusinessCriticality::VeryHigh => "VERY_HIGH",
252            BusinessCriticality::High => "HIGH", 
253            BusinessCriticality::Medium => "MEDIUM",
254            BusinessCriticality::Low => "LOW",
255            BusinessCriticality::VeryLow => "VERY_LOW",
256        }
257    }
258}
259
260impl From<BusinessCriticality> for String {
261    fn from(criticality: BusinessCriticality) -> Self {
262        criticality.as_str().to_string()
263    }
264}
265
266impl std::fmt::Display for BusinessCriticality {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        write!(f, "{}", self.as_str())
269    }
270}
271
272/// Custom serializer for BusinessCriticality
273fn serialize_business_criticality<S>(criticality: &BusinessCriticality, serializer: S) -> Result<S::Ok, S::Error>
274where
275    S: serde::Serializer,
276{
277    serializer.serialize_str(criticality.as_str())
278}
279
280/// Parse BusinessCriticality from string
281impl std::str::FromStr for BusinessCriticality {
282    type Err = String;
283
284    fn from_str(s: &str) -> Result<Self, Self::Err> {
285        match s {
286            "VERY_HIGH" => Ok(BusinessCriticality::VeryHigh),
287            "HIGH" => Ok(BusinessCriticality::High),
288            "MEDIUM" => Ok(BusinessCriticality::Medium),
289            "LOW" => Ok(BusinessCriticality::Low),
290            "VERY_LOW" => Ok(BusinessCriticality::VeryLow),
291            _ => Err(format!("Invalid business criticality: '{s}'. Must be one of: VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW")),
292        }
293    }
294}
295
296/// Deserialize BusinessCriticality from string
297impl<'de> serde::Deserialize<'de> for BusinessCriticality {
298    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
299    where
300        D: serde::Deserializer<'de>,
301    {
302        let s = String::deserialize(deserializer)?;
303        s.parse().map_err(serde::de::Error::custom)
304    }
305}
306
307/// Request for updating an application.
308#[derive(Debug, Serialize, Deserialize, Clone)]
309pub struct UpdateApplicationRequest {
310    /// Application profile information
311    pub profile: UpdateApplicationProfile,
312}
313
314/// Profile information for updating an application.
315#[derive(Debug, Serialize, Deserialize, Clone)]
316pub struct UpdateApplicationProfile {
317    /// Application name
318    pub name: Option<String>,
319    /// Application description
320    pub description: Option<String>,
321    /// Business unit
322    pub business_unit: Option<BusinessUnit>,
323    /// Business owners
324    pub business_owners: Option<Vec<BusinessOwner>>,
325    /// Business criticality level (required)
326    #[serde(serialize_with = "serialize_business_criticality")]
327    pub business_criticality: BusinessCriticality,
328    /// Policies
329    pub policies: Option<Vec<Policy>>,
330    /// Teams
331    pub teams: Option<Vec<Team>>,
332    /// Tags
333    pub tags: Option<String>,
334    /// Custom fields
335    pub custom_fields: Option<Vec<CustomField>>,
336}
337
338/// Query parameters for filtering applications.
339#[derive(Debug, Clone, Default)]
340pub struct ApplicationQuery {
341    /// Filter by application name (partial match)
342    pub name: Option<String>,
343    /// Filter by policy compliance status (PASSED, DID_NOT_PASS, etc.)
344    pub policy_compliance: Option<String>,
345    /// Filter applications modified after this date (ISO 8601 format)
346    pub modified_after: Option<String>,
347    /// Filter applications modified before this date (ISO 8601 format)
348    pub modified_before: Option<String>,
349    /// Filter applications created after this date (ISO 8601 format)
350    pub created_after: Option<String>,
351    /// Filter applications created before this date (ISO 8601 format)
352    pub created_before: Option<String>,
353    /// Filter by scan type (STATIC, DYNAMIC, MANUAL, SCA)
354    pub scan_type: Option<String>,
355    /// Filter by tags (comma-separated)
356    pub tags: Option<String>,
357    /// Filter by business unit name
358    pub business_unit: Option<String>,
359    /// Page number for pagination (0-based)
360    pub page: Option<u32>,
361    /// Number of items per page (default: 20, max: 500)
362    pub size: Option<u32>,
363}
364
365impl ApplicationQuery {
366    /// Create a new empty query.
367    pub fn new() -> Self {
368        Default::default()
369    }
370
371    /// Filter applications by name (partial match).
372    pub fn with_name(mut self, name: String) -> Self {
373        self.name = Some(name);
374        self
375    }
376
377    /// Filter applications by policy compliance status.
378    pub fn with_policy_compliance(mut self, compliance: String) -> Self {
379        self.policy_compliance = Some(compliance);
380        self
381    }
382
383    /// Filter applications modified after the specified date.
384    pub fn with_modified_after(mut self, date: String) -> Self {
385        self.modified_after = Some(date);
386        self
387    }
388
389    /// Filter applications modified before the specified date.
390    pub fn with_modified_before(mut self, date: String) -> Self {
391        self.modified_before = Some(date);
392        self
393    }
394
395    /// Set the page number for pagination.
396    pub fn with_page(mut self, page: u32) -> Self {
397        self.page = Some(page);
398        self
399    }
400
401    /// Set the number of items per page.
402    pub fn with_size(mut self, size: u32) -> Self {
403        self.size = Some(size);
404        self
405    }
406
407    /// Convert the query to URL query parameters.
408    pub fn to_query_params(&self) -> Vec<(String, String)> {
409        let mut params = Vec::new();
410        
411        if let Some(ref name) = self.name {
412            params.push(("name".to_string(), name.clone()));
413        }
414        if let Some(ref compliance) = self.policy_compliance {
415            params.push(("policy_compliance".to_string(), compliance.clone()));
416        }
417        if let Some(ref date) = self.modified_after {
418            params.push(("modified_after".to_string(), date.clone()));
419        }
420        if let Some(ref date) = self.modified_before {
421            params.push(("modified_before".to_string(), date.clone()));
422        }
423        if let Some(ref date) = self.created_after {
424            params.push(("created_after".to_string(), date.clone()));
425        }
426        if let Some(ref date) = self.created_before {
427            params.push(("created_before".to_string(), date.clone()));
428        }
429        if let Some(ref scan_type) = self.scan_type {
430            params.push(("scan_type".to_string(), scan_type.clone()));
431        }
432        if let Some(ref tags) = self.tags {
433            params.push(("tags".to_string(), tags.clone()));
434        }
435        if let Some(ref business_unit) = self.business_unit {
436            params.push(("business_unit".to_string(), business_unit.clone()));
437        }
438        if let Some(page) = self.page {
439            params.push(("page".to_string(), page.to_string()));
440        }
441        if let Some(size) = self.size {
442            params.push(("size".to_string(), size.to_string()));
443        }
444
445        params
446    }
447}
448
449/// Application-specific methods that build on the core client.
450impl VeracodeClient {
451    /// Get all applications with optional filtering.
452    ///
453    /// # Arguments
454    ///
455    /// * `query` - Optional query parameters to filter the results
456    ///
457    /// # Returns
458    ///
459    /// A `Result` containing an `ApplicationsResponse` with the list of applications.
460    pub async fn get_applications(&self, query: Option<ApplicationQuery>) -> Result<ApplicationsResponse, VeracodeError> {
461        let endpoint = "/appsec/v1/applications";
462        let query_params = query.as_ref().map(|q| q.to_query_params());
463        
464        let response = self.get(endpoint, query_params.as_deref()).await?;
465        let response = Self::handle_response(response).await?;
466        
467        let apps_response: ApplicationsResponse = response.json().await?;
468        Ok(apps_response)
469    }
470
471    /// Get a specific application by its GUID.
472    ///
473    /// # Arguments
474    ///
475    /// * `guid` - The GUID of the application to retrieve
476    ///
477    /// # Returns
478    ///
479    /// A `Result` containing the `Application` details.
480    pub async fn get_application(&self, guid: &str) -> Result<Application, VeracodeError> {
481        let endpoint = format!("/appsec/v1/applications/{guid}");
482        
483        let response = self.get(&endpoint, None).await?;
484        let response = Self::handle_response(response).await?;
485        
486        let app: Application = response.json().await?;
487        Ok(app)
488    }
489
490    /// Create a new application.
491    ///
492    /// # Arguments
493    ///
494    /// * `request` - The application creation request containing profile information
495    ///
496    /// # Returns
497    ///
498    /// A `Result` containing the created `Application`.
499    pub async fn create_application(&self, request: CreateApplicationRequest) -> Result<Application, VeracodeError> {
500        let endpoint = "/appsec/v1/applications";
501        
502        let response = self.post(endpoint, Some(&request)).await?;
503        let response = Self::handle_response(response).await?;
504        
505        let app: Application = response.json().await?;
506        Ok(app)
507    }
508
509    /// Update an existing application.
510    ///
511    /// # Arguments
512    ///
513    /// * `guid` - The GUID of the application to update
514    /// * `request` - The update request containing the new profile information
515    ///
516    /// # Returns
517    ///
518    /// A `Result` containing the updated `Application`.
519    pub async fn update_application(&self, guid: &str, request: UpdateApplicationRequest) -> Result<Application, VeracodeError> {
520        let endpoint = format!("/appsec/v1/applications/{guid}");
521        
522        let response = self.put(&endpoint, Some(&request)).await?;
523        let response = Self::handle_response(response).await?;
524        
525        let app: Application = response.json().await?;
526        Ok(app)
527    }
528
529    /// Delete an application.
530    ///
531    /// # Arguments
532    ///
533    /// * `guid` - The GUID of the application to delete
534    ///
535    /// # Returns
536    ///
537    /// A `Result` indicating success or failure.
538    pub async fn delete_application(&self, guid: &str) -> Result<(), VeracodeError> {
539        let endpoint = format!("/appsec/v1/applications/{guid}");
540        
541        let response = self.delete(&endpoint).await?;
542        let _response = Self::handle_response(response).await?;
543        
544        Ok(())
545    }
546
547    /// Get applications that failed policy compliance.
548    ///
549    /// # Returns
550    ///
551    /// A `Result` containing a `Vec<Application>` of non-compliant applications.
552    pub async fn get_non_compliant_applications(&self) -> Result<Vec<Application>, VeracodeError> {
553        let query = ApplicationQuery::new()
554            .with_policy_compliance("DID_NOT_PASS".to_string());
555        
556        let response = self.get_applications(Some(query)).await?;
557        
558        if let Some(embedded) = response.embedded {
559            Ok(embedded.applications)
560        } else {
561            Ok(Vec::new())
562        }
563    }
564
565    /// Get applications modified after a specific date.
566    ///
567    /// # Arguments
568    ///
569    /// * `date` - ISO 8601 formatted date string
570    ///
571    /// # Returns
572    ///
573    /// A `Result` containing a `Vec<Application>` of applications modified after the date.
574    pub async fn get_applications_modified_after(&self, date: &str) -> Result<Vec<Application>, VeracodeError> {
575        let query = ApplicationQuery::new()
576            .with_modified_after(date.to_string());
577        
578        let response = self.get_applications(Some(query)).await?;
579        
580        if let Some(embedded) = response.embedded {
581            Ok(embedded.applications)
582        } else {
583            Ok(Vec::new())
584        }
585    }
586
587    /// Search applications by name.
588    ///
589    /// # Arguments
590    ///
591    /// * `name` - The name to search for (partial matches are supported)
592    ///
593    /// # Returns
594    ///
595    /// A `Result` containing a `Vec<Application>` of applications matching the name.
596    pub async fn search_applications_by_name(&self, name: &str) -> Result<Vec<Application>, VeracodeError> {
597        let query = ApplicationQuery::new()
598            .with_name(name.to_string());
599        
600        let response = self.get_applications(Some(query)).await?;
601        
602        if let Some(embedded) = response.embedded {
603            Ok(embedded.applications)
604        } else {
605            Ok(Vec::new())
606        }
607    }
608
609    /// Get all applications with automatic pagination.
610    ///
611    /// # Returns
612    ///
613    /// A `Result` containing a `Vec<Application>` of all applications.
614    pub async fn get_all_applications(&self) -> Result<Vec<Application>, VeracodeError> {
615        let mut all_applications = Vec::new();
616        let mut page = 0;
617        
618        loop {
619            let query = ApplicationQuery::new()
620                .with_page(page)
621                .with_size(100);
622            
623            let response = self.get_applications(Some(query)).await?;
624            
625            if let Some(embedded) = response.embedded {
626                if embedded.applications.is_empty() {
627                    break;
628                }
629                all_applications.extend(embedded.applications);
630                page += 1;
631            } else {
632                break;
633            }
634        }
635        
636        Ok(all_applications)
637    }
638
639    /// Get application by name (exact match).
640    ///
641    /// # Arguments
642    ///
643    /// * `name` - The exact name of the application to find
644    ///
645    /// # Returns
646    ///
647    /// A `Result` containing an `Option<Application>` if found.
648    pub async fn get_application_by_name(&self, name: &str) -> Result<Option<Application>, VeracodeError> {
649        let applications = self.search_applications_by_name(name).await?;
650        
651        // Find exact match (search_applications_by_name does partial matching)
652        Ok(applications.into_iter().find(|app| {
653            if let Some(profile) = &app.profile {
654                profile.name == name
655            } else {
656                false
657            }
658        }))
659    }
660
661    /// Check if application exists by name.
662    ///
663    /// # Arguments
664    ///
665    /// * `name` - The name of the application to check
666    ///
667    /// # Returns
668    ///
669    /// A `Result` containing a boolean indicating if the application exists.
670    pub async fn application_exists_by_name(&self, name: &str) -> Result<bool, VeracodeError> {
671        match self.get_application_by_name(name).await? {
672            Some(_) => Ok(true),
673            None => Ok(false),
674        }
675    }
676
677    /// Get numeric app_id from application GUID.
678    ///
679    /// This is needed for XML API operations that require numeric IDs.
680    ///
681    /// # Arguments
682    ///
683    /// * `guid` - The application GUID
684    ///
685    /// # Returns
686    ///
687    /// A `Result` containing the numeric app_id as a string.
688    pub async fn get_app_id_from_guid(&self, guid: &str) -> Result<String, VeracodeError> {
689        let app = self.get_application(guid).await?;
690        Ok(app.id.to_string())
691    }
692
693    /// Create application if it doesn't exist, or return existing application.
694    ///
695    /// This method implements the "check and create" pattern commonly needed
696    /// for automated workflows.
697    ///
698    /// # Arguments
699    ///
700    /// * `name` - The name of the application
701    /// * `business_criticality` - Business criticality level (required for creation)
702    /// * `description` - Optional description for new applications
703    ///
704    /// # Returns
705    ///
706    /// A `Result` containing the application (existing or newly created).
707    pub async fn create_application_if_not_exists(
708        &self,
709        name: &str,
710        business_criticality: BusinessCriticality,
711        description: Option<String>,
712    ) -> Result<Application, VeracodeError> {
713        // First, check if application already exists
714        if let Some(existing_app) = self.get_application_by_name(name).await? {
715            return Ok(existing_app);
716        }
717
718        // Application doesn't exist, create it
719        let create_request = CreateApplicationRequest {
720            profile: CreateApplicationProfile {
721                name: name.to_string(),
722                business_criticality,
723                description,
724                business_unit: None,
725                business_owners: None,
726                policies: None,
727                teams: None,
728                tags: None,
729                custom_fields: None,
730            },
731        };
732
733        self.create_application(create_request).await
734    }
735}
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740
741    #[test]
742    fn test_query_params() {
743        let query = ApplicationQuery::new()
744            .with_name("test_app".to_string())
745            .with_policy_compliance("PASSED".to_string())
746            .with_page(1)
747            .with_size(50);
748        
749        let params = query.to_query_params();
750        assert!(params.contains(&("name".to_string(), "test_app".to_string())));
751        assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
752        assert!(params.contains(&("page".to_string(), "1".to_string())));
753        assert!(params.contains(&("size".to_string(), "50".to_string())));
754    }
755
756    #[test]
757    fn test_application_query_builder() {
758        let query = ApplicationQuery::new()
759            .with_name("MyApp".to_string())
760            .with_policy_compliance("DID_NOT_PASS".to_string())
761            .with_modified_after("2023-01-01T00:00:00.000Z".to_string())
762            .with_page(2)
763            .with_size(25);
764        
765        assert_eq!(query.name, Some("MyApp".to_string()));
766        assert_eq!(query.policy_compliance, Some("DID_NOT_PASS".to_string()));
767        assert_eq!(query.modified_after, Some("2023-01-01T00:00:00.000Z".to_string()));
768        assert_eq!(query.page, Some(2));
769        assert_eq!(query.size, Some(25));
770    }
771}