clawspec_core/
lib.rs

1//! # Clawspec Core
2//!
3//! Generate OpenAPI specifications from your HTTP client test code.
4//!
5//! This crate provides two main ways to generate OpenAPI documentation:
6//! - **[`ApiClient`]** - Direct HTTP client for fine-grained control
7//! - **[`TestClient`](test_client::TestClient)** - Test server integration with automatic lifecycle management
8//!
9//! ## Quick Start
10//!
11//! ### Using ApiClient directly
12//!
13//! ```rust,no_run
14//! use clawspec_core::ApiClient;
15//! # use serde::Deserialize;
16//! # use utoipa::ToSchema;
17//! # #[derive(Deserialize, ToSchema)]
18//! # struct User { id: u32, name: String }
19//!
20//! # #[tokio::main]
21//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! let mut client = ApiClient::builder()
23//!     .with_host("api.example.com")
24//!     .build()?;
25//!
26//! // Make requests - schemas are captured automatically  
27//! let user: User = client
28//!     .get("/users/123")?
29//!     .await?  // ← Direct await using IntoFuture
30//!     .as_json()  // ← Important: Must consume result for OpenAPI generation!
31//!     .await?;
32//!
33//! // Generate OpenAPI specification
34//! let spec = client.collected_openapi().await;
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! ### Using TestClient with a test server
40//!
41//! For a complete working example, see the [axum example](https://github.com/ilaborie/clawspec/tree/main/examples/axum-example).
42//!
43//! ```rust,no_run
44//! use clawspec_core::test_client::{TestClient, TestServer};
45//! use std::net::TcpListener;
46//!
47//! # #[derive(Debug)]
48//! # struct MyServer;
49//! # impl TestServer for MyServer {
50//! #     type Error = std::io::Error;
51//! #     async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
52//! #         Ok(())
53//! #     }
54//! # }
55//! #[tokio::test]
56//! async fn test_api() -> Result<(), Box<dyn std::error::Error>> {
57//!     let mut client = TestClient::start(MyServer).await?;
58//!     
59//!     // Test your API
60//!     let response = client.get("/users")?.await?.as_json::<serde_json::Value>().await?;
61//!     
62//!     // Write OpenAPI spec
63//!     client.write_openapi("api.yml").await?;
64//!     Ok(())
65//! }
66//! ```
67//!
68//! ## Working with Parameters
69//!
70//! ```rust
71//! use clawspec_core::{ApiClient, CallPath, CallQuery, CallHeaders, CallCookies, ParamValue, ParamStyle};
72//!
73//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
74//! // Path parameters  
75//! let path = CallPath::from("/users/{id}")
76//!     .add_param("id", ParamValue::new(123));
77//!
78//! // Query parameters
79//! let query = CallQuery::new()
80//!     .add_param("page", ParamValue::new(1))
81//!     .add_param("limit", ParamValue::new(10));
82//!
83//! // Headers
84//! let headers = CallHeaders::new()
85//!     .add_header("Authorization", "Bearer token");
86//!
87//! // Cookies
88//! let cookies = CallCookies::new()
89//!     .add_cookie("session_id", "abc123")
90//!     .add_cookie("user_id", 456);
91//!
92//! // Direct await with parameters:
93//! let response = client
94//!     .get(path)?
95//!     .with_query(query)
96//!     .with_headers(headers)
97//!     .with_cookies(cookies)
98//!     .await?;  // Direct await using IntoFuture
99//! # Ok(())
100//! # }
101//! ```
102//!
103//! ## OpenAPI 3.1.0 Parameter Styles
104//!
105//! This library supports all OpenAPI 3.1.0 parameter styles for different parameter types:
106//!
107//! ### Path Parameters
108//!
109//! ```rust
110//! use clawspec_core::{CallPath, ParamValue, ParamStyle};
111//!
112//! # async fn example() {
113//! // Simple style (default): /users/123
114//! let path = CallPath::from("/users/{id}")
115//!     .add_param("id", ParamValue::new(123));
116//!
117//! // Label style: /users/.123
118//! let path = CallPath::from("/users/{id}")
119//!     .add_param("id", ParamValue::with_style(123, ParamStyle::Label));
120//!
121//! // Matrix style: /users/;id=123
122//! let path = CallPath::from("/users/{id}")
123//!     .add_param("id", ParamValue::with_style(123, ParamStyle::Matrix));
124//!
125//! // Arrays with different styles
126//! let tags = vec!["rust", "web", "api"];
127//!
128//! // Simple: /search/rust,web,api
129//! let path = CallPath::from("/search/{tags}")
130//!     .add_param("tags", ParamValue::with_style(tags.clone(), ParamStyle::Simple));
131//!
132//! // Label: /search/.rust,web,api
133//! let path = CallPath::from("/search/{tags}")
134//!     .add_param("tags", ParamValue::with_style(tags.clone(), ParamStyle::Label));
135//!
136//! // Matrix: /search/;tags=rust,web,api
137//! let path = CallPath::from("/search/{tags}")
138//!     .add_param("tags", ParamValue::with_style(tags, ParamStyle::Matrix));
139//! # }
140//! ```
141//!
142//! ### Query Parameters
143//!
144//! ```rust
145//! use clawspec_core::{CallQuery, ParamValue, ParamStyle};
146//!
147//! # async fn example() {
148//! let tags = vec!["rust", "web", "api"];
149//!
150//! // Form style (default): ?tags=rust&tags=web&tags=api
151//! let query = CallQuery::new()
152//!     .add_param("tags", ParamValue::new(tags.clone()));
153//!
154//! // Space delimited: ?tags=rust%20web%20api
155//! let query = CallQuery::new()
156//!     .add_param("tags", ParamValue::with_style(tags.clone(), ParamStyle::SpaceDelimited));
157//!
158//! // Pipe delimited: ?tags=rust|web|api
159//! let query = CallQuery::new()
160//!     .add_param("tags", ParamValue::with_style(tags, ParamStyle::PipeDelimited));
161//!
162//! // Deep object style: ?user[name]=john&user[age]=30
163//! let user_data = serde_json::json!({"name": "john", "age": 30});
164//! let query = CallQuery::new()
165//!     .add_param("user", ParamValue::with_style(user_data, ParamStyle::DeepObject));
166//! # }
167//! ```
168//!
169//! ### Cookie Parameters
170//!
171//! ```rust
172//! use clawspec_core::{CallCookies, ParamValue};
173//!
174//! # async fn example() {
175//! // Simple cookie values
176//! let cookies = CallCookies::new()
177//!     .add_cookie("session_id", "abc123")
178//!     .add_cookie("user_id", 456)
179//!     .add_cookie("is_admin", true);
180//!
181//! // Array values in cookies (comma-separated)
182//! let cookies = CallCookies::new()
183//!     .add_cookie("preferences", vec!["dark_mode", "notifications"])
184//!     .add_cookie("selected_tags", vec!["rust", "web", "api"]);
185//!
186//! // Custom types with automatic serialization
187//! #[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
188//! struct UserId(u64);
189//!
190//! let cookies = CallCookies::new()
191//!     .add_cookie("user", UserId(12345));
192//! # }
193//! ```
194//!
195//! ## Authentication
196//!
197//! The library supports various authentication methods that can be configured at the client level
198//! or overridden for individual requests.
199//!
200//! ### Client-Level Authentication
201//!
202//! ```rust
203//! use clawspec_core::{ApiClient, Authentication};
204//!
205//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
206//! // Bearer token authentication
207//! let client = ApiClient::builder()
208//!     .with_host("api.example.com")
209//!     .with_authentication(Authentication::Bearer("my-api-token".into()))
210//!     .build()?;
211//!
212//! // Basic authentication
213//! let client = ApiClient::builder()
214//!     .with_host("api.example.com")
215//!     .with_authentication(Authentication::Basic {
216//!         username: "user".to_string(),
217//!         password: "pass".into(),
218//!     })
219//!     .build()?;
220//!
221//! // API key authentication
222//! let client = ApiClient::builder()
223//!     .with_host("api.example.com")
224//!     .with_authentication(Authentication::ApiKey {
225//!         header_name: "X-API-Key".to_string(),
226//!         key: "secret-key".into(),
227//!     })
228//!     .build()?;
229//! # Ok(())
230//! # }
231//! ```
232//!
233//! ### Per-Request Authentication Override
234//!
235//! ```rust
236//! use clawspec_core::{ApiClient, Authentication};
237//!
238//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
239//! // Client with default authentication
240//! let mut client = ApiClient::builder()
241//!     .with_host("api.example.com")
242//!     .with_authentication(Authentication::Bearer("default-token".into()))
243//!     .build()?;
244//!
245//! // Use different authentication for admin endpoints
246//! let admin_users = client
247//!     .get("/admin/users")?
248//!     .with_authentication(Authentication::Bearer("admin-token".into()))
249//!     .await?
250//!     .as_json::<serde_json::Value>()
251//!     .await?;
252//!
253//! // Remove authentication for public endpoints
254//! let public_data = client
255//!     .get("/public/health")?
256//!     .with_authentication_none()
257//!     .await?
258//!     .as_text()
259//!     .await?;
260//! # Ok(())
261//! # }
262//! ```
263//!
264//! ### Authentication Types
265//!
266//! - **Bearer**: Adds `Authorization: Bearer <token>` header
267//! - **Basic**: Adds `Authorization: Basic <base64(username:password)>` header  
268//! - **ApiKey**: Adds custom header with API key
269//!
270//! ### Security Best Practices
271//!
272//! - Store credentials securely using environment variables or secret management tools
273//! - Rotate tokens regularly
274//! - Use HTTPS for all authenticated requests
275//! - Avoid logging authentication headers
276//!
277//! ## Status Code Validation
278//!
279//! By default, requests expect status codes in the range 200-499 (inclusive of 200, exclusive of 500).
280//! You can customize this behavior:
281//!
282//! ```rust
283//! use clawspec_core::{ApiClient, expected_status_codes};
284//!
285//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
286//! // Single codes
287//! client.post("/users")?
288//!     .with_expected_status_codes(expected_status_codes!(201, 202))
289//!     
290//!     .await?;
291//!
292//! // Ranges
293//! client.get("/health")?
294//!     .with_expected_status_codes(expected_status_codes!(200-299))
295//!     
296//!     .await?;
297//! # Ok(())
298//! # }
299//! ```
300//!
301//! ## Response Descriptions
302//!
303//! Add descriptive text to your OpenAPI responses for better documentation:
304//!
305//! ```rust
306//! use clawspec_core::ApiClient;
307//!
308//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
309//! // Set a description for the actual returned status code
310//! client.get("/users/{id}")?
311//!     .with_response_description("User details if found, or error information")
312//!     .await?;
313//!
314//! // The description applies to whatever status code is actually returned
315//! client.post("/users")?
316//!     .with_response_description("User created successfully or validation error")
317//!     .await?;
318//! # Ok(())
319//! # }
320//! ```
321//!
322//! ## Schema Registration
323//!
324//! ### Automatic Schema Capture
325//!
326//! JSON request and response body schemas are **automatically captured** when using `.json()` and `.as_json()` methods:
327//!
328//! ```rust
329//! use clawspec_core::ApiClient;
330//! # use serde::{Serialize, Deserialize};
331//! # use utoipa::ToSchema;
332//!
333//! #[derive(Serialize, Deserialize, ToSchema)]
334//! struct CreateUser { name: String, email: String }
335//!
336//! #[derive(Deserialize, ToSchema)]
337//! struct User { id: u32, name: String, email: String }
338//!
339//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
340//! // Schemas are captured automatically - no explicit registration needed
341//! let user: User = client
342//!     .post("/users")?
343//!     .json(&CreateUser { name: "Alice".to_string(), email: "alice@example.com".to_string() })?
344//!     .await?
345//!     .as_json()
346//!     .await?;
347//! # Ok(())
348//! # }
349//! ```
350//!
351//! ### Manual Schema Registration
352//!
353//! For nested schemas or when you need to ensure all dependencies are included, use the `register_schemas!` macro:
354//!
355//! ```rust
356//! use clawspec_core::{ApiClient, register_schemas};
357//! # use serde::{Serialize, Deserialize};
358//! # use utoipa::ToSchema;
359//!
360//! #[derive(Serialize, Deserialize, ToSchema)]
361//! struct Address { street: String, city: String }
362//!
363//! #[derive(Serialize, Deserialize, ToSchema)]
364//! struct CreateUser { name: String, email: String, address: Address }
365//!
366//! #[derive(Deserialize, ToSchema)]
367//! struct ErrorResponse { code: String, message: String }
368//!
369//! # async fn example(client: &mut ApiClient) {
370//! // Register nested schemas and error types for complete documentation
371//! register_schemas!(client, CreateUser, Address, ErrorResponse).await;
372//! # }
373//! ```
374//!
375//! ### ⚠️ Nested Schema Limitation
376//!
377//! **Current Limitation**: While main JSON body schemas are captured automatically, nested schemas may not be fully resolved. If you encounter missing nested schemas in your OpenAPI specification, use the `register_schemas!` macro to explicitly register them:
378//!
379//! ```rust
380//! use clawspec_core::{ApiClient, register_schemas};
381//! # use serde::{Serialize, Deserialize};
382//! # use utoipa::ToSchema;
383//!
384//! #[derive(Serialize, Deserialize, ToSchema)]
385//! struct Position { lat: f64, lng: f64 }
386//!
387//! #[derive(Serialize, Deserialize, ToSchema)]
388//! struct Location { name: String, position: Position }  // Position is nested
389//!
390//! # async fn example(client: &mut ApiClient) {
391//! // Register both main and nested schemas to ensure complete OpenAPI generation
392//! register_schemas!(client, Location, Position).await;
393//! # }
394//! ```
395//!
396//! **Workaround**: Always register nested schemas explicitly when you need complete OpenAPI documentation with all referenced types properly defined.
397//!
398//! ## Error Handling
399//!
400//! The library provides two main error types:
401//! - [`ApiClientError`] - HTTP client errors (network, parsing, validation)
402//! - [`TestAppError`](test_client::TestAppError) - Test server lifecycle errors
403//!
404//! ## See Also
405//!
406//! - [`ApiClient`] - HTTP client with OpenAPI collection
407//! - [`ApiCall`] - Request builder with parameter support
408//! - [`test_client`] - Test server integration module
409//! - [`ExpectedStatusCodes`] - Status code validation
410//!
411//! ## Re-exports
412//!
413//! All commonly used types are re-exported from the crate root for convenience.
414
415// TODO: Add comprehensive unit tests for all modules - https://github.com/ilaborie/clawspec/issues/30
416
417mod client;
418
419pub mod test_client;
420
421// Public API - only expose user-facing types and functions
422pub use self::client::{
423    ApiCall, ApiClient, ApiClientBuilder, ApiClientError, Authentication, AuthenticationError,
424    CallBody, CallCookies, CallHeaders, CallPath, CallQuery, CallResult, ExpectedStatusCodes,
425    ParamStyle, ParamValue, ParameterValue, RawBody, RawResult, SecureString,
426};
427
428// Convenience macro re-exports are handled by the macro_rules! definitions below
429
430/// Creates an [`ExpectedStatusCodes`] instance with the specified status codes and ranges.
431///
432/// This macro provides a convenient syntax for defining expected HTTP status codes
433/// with support for individual codes, inclusive ranges, and exclusive ranges.
434///
435/// # Syntax
436///
437/// - Single codes: `200`, `201`, `404`
438/// - Inclusive ranges: `200-299` (includes both endpoints)
439/// - Exclusive ranges: `200..300` (excludes 300)
440/// - Mixed: `200, 201-204, 400..500`
441///
442/// # Examples
443///
444/// ```rust
445/// use clawspec_core::expected_status_codes;
446///
447/// // Single status codes
448/// let codes = expected_status_codes!(200, 201, 204);
449///
450/// // Ranges
451/// let success_codes = expected_status_codes!(200-299);
452/// let client_errors = expected_status_codes!(400..500);
453///
454/// // Mixed
455/// let mixed = expected_status_codes!(200-204, 301, 302, 400-404);
456/// ```
457#[macro_export]
458macro_rules! expected_status_codes {
459    // Empty case
460    () => {
461        $crate::ExpectedStatusCodes::default()
462    };
463
464    // Single element
465    ($single:literal) => {
466        $crate::ExpectedStatusCodes::from_single($single)
467    };
468
469    // Single range (inclusive)
470    ($start:literal - $end:literal) => {
471        $crate::ExpectedStatusCodes::from_inclusive_range($start..=$end)
472    };
473
474    // Single range (exclusive)
475    ($start:literal .. $end:literal) => {
476        $crate::ExpectedStatusCodes::from_exclusive_range($start..$end)
477    };
478
479    // Multiple elements - single code followed by more
480    ($first:literal, $($rest:tt)*) => {{
481        #[allow(unused_mut)]
482        let mut codes = $crate::ExpectedStatusCodes::from_single($first);
483        $crate::expected_status_codes!(@accumulate codes, $($rest)*);
484        codes
485    }};
486
487    // Multiple elements - inclusive range followed by more
488    ($start:literal - $end:literal, $($rest:tt)*) => {{
489        #[allow(unused_mut)]
490        let mut codes = $crate::ExpectedStatusCodes::from_inclusive_range($start..=$end);
491        $crate::expected_status_codes!(@accumulate codes, $($rest)*);
492        codes
493    }};
494
495    // Multiple elements - exclusive range followed by more
496    ($start:literal .. $end:literal, $($rest:tt)*) => {{
497        #[allow(unused_mut)]
498        let mut codes = $crate::ExpectedStatusCodes::from_exclusive_range($start..$end);
499        $crate::expected_status_codes!(@accumulate codes, $($rest)*);
500        codes
501    }};
502
503    // Internal accumulator - empty (base case for trailing commas)
504    (@accumulate $codes:ident,) => {
505        // Do nothing for trailing commas
506    };
507
508    // Internal accumulator - single code
509    (@accumulate $codes:ident, $single:literal) => {
510        $codes = $codes.add_single($single);
511    };
512
513    // Internal accumulator - single code followed by more
514    (@accumulate $codes:ident, $single:literal, $($rest:tt)*) => {
515        $codes = $codes.add_single($single);
516        $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
517    };
518
519    // Internal accumulator - inclusive range
520    (@accumulate $codes:ident, $start:literal - $end:literal) => {
521        $codes = $codes.add_inclusive_range($start..=$end);
522    };
523
524    // Internal accumulator - inclusive range followed by more
525    (@accumulate $codes:ident, $start:literal - $end:literal, $($rest:tt)*) => {
526        $codes = $codes.add_inclusive_range($start..=$end);
527        $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
528    };
529
530    // Internal accumulator - exclusive range
531    (@accumulate $codes:ident, $start:literal .. $end:literal) => {
532        $codes = $codes.add_exclusive_range($start..$end);
533    };
534
535    // Internal accumulator - exclusive range followed by more
536    (@accumulate $codes:ident, $start:literal .. $end:literal, $($rest:tt)*) => {
537        $codes = $codes.add_exclusive_range($start..$end);
538        $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
539    };
540
541    // Internal accumulator - empty (catch all for trailing cases)
542    (@accumulate $codes:ident) => {
543        // Base case - do nothing
544    };
545}
546
547/// Registers multiple schema types with the ApiClient for OpenAPI documentation.
548///
549/// This macro simplifies the process of registering multiple types that implement
550/// [`utoipa::ToSchema`] with an [`ApiClient`] instance.
551///
552/// # When to Use
553///
554/// - **Nested Schemas**: When your JSON types contain nested structures that need to be fully resolved
555/// - **Error Types**: To ensure error response schemas are included in the OpenAPI specification
556/// - **Complex Dependencies**: When automatic schema capture doesn't include all referenced types
557///
558/// # Automatic vs Manual Registration
559///
560/// Most JSON request/response schemas are captured automatically when using `.json()` and `.as_json()` methods.
561/// Use this macro when you need to ensure complete schema coverage, especially for nested types.
562///
563/// # Examples
564///
565/// ## Basic Usage
566///
567/// ```rust
568/// use clawspec_core::{ApiClient, register_schemas};
569/// use serde::{Serialize, Deserialize};
570/// use utoipa::ToSchema;
571///
572/// #[derive(Serialize, Deserialize, ToSchema)]
573/// struct User {
574///     id: u64,
575///     name: String,
576/// }
577///
578/// #[derive(Serialize, Deserialize, ToSchema)]
579/// struct Post {
580///     id: u64,
581///     title: String,
582///     author_id: u64,
583/// }
584///
585/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
586/// let mut client = ApiClient::builder().build()?;
587///
588/// // Register multiple schemas at once
589/// register_schemas!(client, User, Post).await;
590/// # Ok(())
591/// # }
592/// ```
593///
594/// ## Nested Schemas
595///
596/// ```rust
597/// use clawspec_core::{ApiClient, register_schemas};
598/// use serde::{Serialize, Deserialize};
599/// use utoipa::ToSchema;
600///
601/// #[derive(Serialize, Deserialize, ToSchema)]
602/// struct Address {
603///     street: String,
604///     city: String,
605/// }
606///
607/// #[derive(Serialize, Deserialize, ToSchema)]
608/// struct User {
609///     id: u64,
610///     name: String,
611///     address: Address,  // Nested schema
612/// }
613///
614/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
615/// let mut client = ApiClient::builder().build()?;
616///
617/// // Register both main and nested schemas for complete OpenAPI generation
618/// register_schemas!(client, User, Address).await;
619/// # Ok(())
620/// # }
621/// ```
622#[macro_export]
623macro_rules! register_schemas {
624    ($client:expr, $($schema:ty),+ $(,)?) => {
625        async {
626            $(
627                $client.register_schema::<$schema>().await;
628            )+
629        }
630    };
631}
632
633#[cfg(test)]
634mod macro_tests {
635    use super::*;
636
637    #[test]
638    fn test_expected_status_codes_single() {
639        let codes = expected_status_codes!(200);
640        assert!(codes.contains(200));
641        assert!(!codes.contains(201));
642    }
643
644    #[test]
645    fn test_expected_status_codes_multiple_single() {
646        let codes = expected_status_codes!(200, 201, 204);
647        assert!(codes.contains(200));
648        assert!(codes.contains(201));
649        assert!(codes.contains(204));
650        assert!(!codes.contains(202));
651    }
652
653    #[test]
654    fn test_expected_status_codes_range() {
655        let codes = expected_status_codes!(200 - 204);
656        assert!(codes.contains(200));
657        assert!(codes.contains(202));
658        assert!(codes.contains(204));
659        assert!(!codes.contains(205));
660    }
661
662    #[test]
663    fn test_expected_status_codes_mixed() {
664        let codes = expected_status_codes!(200, 201 - 204, 301, 400 - 404);
665        assert!(codes.contains(200));
666        assert!(codes.contains(202));
667        assert!(codes.contains(301));
668        assert!(codes.contains(402));
669        assert!(!codes.contains(305));
670    }
671
672    #[test]
673    fn test_expected_status_codes_trailing_comma() {
674        let codes = expected_status_codes!(200, 201,);
675        assert!(codes.contains(200));
676        assert!(codes.contains(201));
677    }
678
679    #[test]
680    fn test_expected_status_codes_range_trailing_comma() {
681        let codes = expected_status_codes!(200 - 204,);
682        assert!(codes.contains(202));
683    }
684
685    #[test]
686    fn test_expected_status_codes_five_elements() {
687        let codes = expected_status_codes!(200, 201, 202, 203, 204);
688        assert!(codes.contains(200));
689        assert!(codes.contains(201));
690        assert!(codes.contains(202));
691        assert!(codes.contains(203));
692        assert!(codes.contains(204));
693    }
694
695    #[test]
696    fn test_expected_status_codes_eight_elements() {
697        let codes = expected_status_codes!(200, 201, 202, 203, 204, 205, 206, 207);
698        assert!(codes.contains(200));
699        assert!(codes.contains(204));
700        assert!(codes.contains(207));
701    }
702
703    #[test]
704    fn test_expected_status_codes_multiple_ranges() {
705        let codes = expected_status_codes!(200 - 204, 300 - 304, 400 - 404);
706        assert!(codes.contains(202));
707        assert!(codes.contains(302));
708        assert!(codes.contains(402));
709        assert!(!codes.contains(205));
710        assert!(!codes.contains(305));
711    }
712
713    #[test]
714    fn test_expected_status_codes_edge_cases() {
715        // Empty should work
716        let _codes = expected_status_codes!();
717
718        // Single range should work
719        let codes = expected_status_codes!(200 - 299);
720        assert!(codes.contains(250));
721    }
722
723    #[test]
724    fn test_expected_status_codes_common_patterns() {
725        // Success codes
726        let success = expected_status_codes!(200 - 299);
727        assert!(success.contains(200));
728        assert!(success.contains(201));
729        assert!(success.contains(204));
730
731        // Client errors
732        let client_errors = expected_status_codes!(400 - 499);
733        assert!(client_errors.contains(400));
734        assert!(client_errors.contains(404));
735        assert!(client_errors.contains(422));
736
737        // Specific success codes
738        let specific = expected_status_codes!(200, 201, 204);
739        assert!(specific.contains(200));
740        assert!(!specific.contains(202));
741    }
742
743    #[test]
744    fn test_expected_status_codes_builder_alternative() {
745        // Using macro
746        let macro_codes = expected_status_codes!(200 - 204, 301, 302, 400 - 404);
747
748        // Using builder (should be equivalent)
749        let builder_codes = ExpectedStatusCodes::default()
750            .add_inclusive_range(200..=204)
751            .add_single(301)
752            .add_single(302)
753            .add_inclusive_range(400..=404);
754
755        // Both should have same results
756        for code in [200, 202, 204, 301, 302, 400, 402, 404] {
757            assert_eq!(macro_codes.contains(code), builder_codes.contains(code));
758        }
759    }
760}
761
762#[cfg(test)]
763mod integration_tests {
764    use super::*;
765
766    #[test]
767    fn test_expected_status_codes_real_world_patterns() {
768        // REST API common patterns
769        let rest_success = expected_status_codes!(200, 201, 204);
770        assert!(rest_success.contains(200)); // GET success
771        assert!(rest_success.contains(201)); // POST created
772        assert!(rest_success.contains(204)); // DELETE success
773
774        // GraphQL typically uses 200 for everything
775        let graphql = expected_status_codes!(200);
776        assert!(graphql.contains(200));
777        assert!(!graphql.contains(201));
778
779        // Health check endpoints
780        let health = expected_status_codes!(200, 503);
781        assert!(health.contains(200)); // Healthy
782        assert!(health.contains(503)); // Unhealthy
783
784        // Authentication endpoints
785        let auth = expected_status_codes!(200, 201, 401, 403);
786        assert!(auth.contains(200)); // Login success
787        assert!(auth.contains(401)); // Unauthorized
788        assert!(auth.contains(403)); // Forbidden
789    }
790
791    #[test]
792    fn test_expected_status_codes_with_api_call() {
793        // This tests that the macro works correctly with actual API calls
794        let client = ApiClient::builder().build().unwrap();
795        let codes = expected_status_codes!(200 - 299, 404);
796
797        // Should compile and be usable
798        let _call = client
799            .get("/test")
800            .unwrap()
801            .with_expected_status_codes(codes);
802    }
803
804    #[test]
805    fn test_expected_status_codes_method_chaining() {
806        let codes = expected_status_codes!(200)
807            .add_single(201)
808            .add_inclusive_range(300..=304);
809
810        assert!(codes.contains(200));
811        assert!(codes.contains(201));
812        assert!(codes.contains(302));
813    }
814
815    #[test]
816    fn test_expected_status_codes_vs_manual_creation() {
817        // Macro version
818        let macro_version = expected_status_codes!(200 - 204, 301, 400);
819
820        // Manual version
821        let manual_version = ExpectedStatusCodes::from_inclusive_range(200..=204)
822            .add_single(301)
823            .add_single(400);
824
825        // Should behave identically
826        for code in 100..600 {
827            assert_eq!(
828                macro_version.contains(code),
829                manual_version.contains(code),
830                "Mismatch for status code {code}"
831            );
832        }
833    }
834}