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#[cfg(feature = "redaction")]
429pub use self::client::{RedactedResult, RedactionBuilder};
430
431// Convenience macro re-exports are handled by the macro_rules! definitions below
432
433/// Creates an [`ExpectedStatusCodes`] instance with the specified status codes and ranges.
434///
435/// This macro provides a convenient syntax for defining expected HTTP status codes
436/// with support for individual codes, inclusive ranges, and exclusive ranges.
437///
438/// # Syntax
439///
440/// - Single codes: `200`, `201`, `404`
441/// - Inclusive ranges: `200-299` (includes both endpoints)
442/// - Exclusive ranges: `200..300` (excludes 300)
443/// - Mixed: `200, 201-204, 400..500`
444///
445/// # Examples
446///
447/// ```rust
448/// use clawspec_core::expected_status_codes;
449///
450/// // Single status codes
451/// let codes = expected_status_codes!(200, 201, 204);
452///
453/// // Ranges
454/// let success_codes = expected_status_codes!(200-299);
455/// let client_errors = expected_status_codes!(400..500);
456///
457/// // Mixed
458/// let mixed = expected_status_codes!(200-204, 301, 302, 400-404);
459/// ```
460#[macro_export]
461macro_rules! expected_status_codes {
462    // Empty case
463    () => {
464        $crate::ExpectedStatusCodes::default()
465    };
466
467    // Single element
468    ($single:literal) => {
469        $crate::ExpectedStatusCodes::from_single($single)
470    };
471
472    // Single range (inclusive)
473    ($start:literal - $end:literal) => {
474        $crate::ExpectedStatusCodes::from_inclusive_range($start..=$end)
475    };
476
477    // Single range (exclusive)
478    ($start:literal .. $end:literal) => {
479        $crate::ExpectedStatusCodes::from_exclusive_range($start..$end)
480    };
481
482    // Multiple elements - single code followed by more
483    ($first:literal, $($rest:tt)*) => {{
484        #[allow(unused_mut)]
485        let mut codes = $crate::ExpectedStatusCodes::from_single($first);
486        $crate::expected_status_codes!(@accumulate codes, $($rest)*);
487        codes
488    }};
489
490    // Multiple elements - inclusive range followed by more
491    ($start:literal - $end:literal, $($rest:tt)*) => {{
492        #[allow(unused_mut)]
493        let mut codes = $crate::ExpectedStatusCodes::from_inclusive_range($start..=$end);
494        $crate::expected_status_codes!(@accumulate codes, $($rest)*);
495        codes
496    }};
497
498    // Multiple elements - exclusive range followed by more
499    ($start:literal .. $end:literal, $($rest:tt)*) => {{
500        #[allow(unused_mut)]
501        let mut codes = $crate::ExpectedStatusCodes::from_exclusive_range($start..$end);
502        $crate::expected_status_codes!(@accumulate codes, $($rest)*);
503        codes
504    }};
505
506    // Internal accumulator - empty (base case for trailing commas)
507    (@accumulate $codes:ident,) => {
508        // Do nothing for trailing commas
509    };
510
511    // Internal accumulator - single code
512    (@accumulate $codes:ident, $single:literal) => {
513        $codes = $codes.add_single($single);
514    };
515
516    // Internal accumulator - single code followed by more
517    (@accumulate $codes:ident, $single:literal, $($rest:tt)*) => {
518        $codes = $codes.add_single($single);
519        $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
520    };
521
522    // Internal accumulator - inclusive range
523    (@accumulate $codes:ident, $start:literal - $end:literal) => {
524        $codes = $codes.add_inclusive_range($start..=$end);
525    };
526
527    // Internal accumulator - inclusive range followed by more
528    (@accumulate $codes:ident, $start:literal - $end:literal, $($rest:tt)*) => {
529        $codes = $codes.add_inclusive_range($start..=$end);
530        $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
531    };
532
533    // Internal accumulator - exclusive range
534    (@accumulate $codes:ident, $start:literal .. $end:literal) => {
535        $codes = $codes.add_exclusive_range($start..$end);
536    };
537
538    // Internal accumulator - exclusive range followed by more
539    (@accumulate $codes:ident, $start:literal .. $end:literal, $($rest:tt)*) => {
540        $codes = $codes.add_exclusive_range($start..$end);
541        $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
542    };
543
544    // Internal accumulator - empty (catch all for trailing cases)
545    (@accumulate $codes:ident) => {
546        // Base case - do nothing
547    };
548}
549
550/// Registers multiple schema types with the ApiClient for OpenAPI documentation.
551///
552/// This macro simplifies the process of registering multiple types that implement
553/// [`utoipa::ToSchema`] with an [`ApiClient`] instance.
554///
555/// # When to Use
556///
557/// - **Nested Schemas**: When your JSON types contain nested structures that need to be fully resolved
558/// - **Error Types**: To ensure error response schemas are included in the OpenAPI specification
559/// - **Complex Dependencies**: When automatic schema capture doesn't include all referenced types
560///
561/// # Automatic vs Manual Registration
562///
563/// Most JSON request/response schemas are captured automatically when using `.json()` and `.as_json()` methods.
564/// Use this macro when you need to ensure complete schema coverage, especially for nested types.
565///
566/// # Examples
567///
568/// ## Basic Usage
569///
570/// ```rust
571/// use clawspec_core::{ApiClient, register_schemas};
572/// use serde::{Serialize, Deserialize};
573/// use utoipa::ToSchema;
574///
575/// #[derive(Serialize, Deserialize, ToSchema)]
576/// struct User {
577///     id: u64,
578///     name: String,
579/// }
580///
581/// #[derive(Serialize, Deserialize, ToSchema)]
582/// struct Post {
583///     id: u64,
584///     title: String,
585///     author_id: u64,
586/// }
587///
588/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
589/// let mut client = ApiClient::builder().build()?;
590///
591/// // Register multiple schemas at once
592/// register_schemas!(client, User, Post).await;
593/// # Ok(())
594/// # }
595/// ```
596///
597/// ## Nested Schemas
598///
599/// ```rust
600/// use clawspec_core::{ApiClient, register_schemas};
601/// use serde::{Serialize, Deserialize};
602/// use utoipa::ToSchema;
603///
604/// #[derive(Serialize, Deserialize, ToSchema)]
605/// struct Address {
606///     street: String,
607///     city: String,
608/// }
609///
610/// #[derive(Serialize, Deserialize, ToSchema)]
611/// struct User {
612///     id: u64,
613///     name: String,
614///     address: Address,  // Nested schema
615/// }
616///
617/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
618/// let mut client = ApiClient::builder().build()?;
619///
620/// // Register both main and nested schemas for complete OpenAPI generation
621/// register_schemas!(client, User, Address).await;
622/// # Ok(())
623/// # }
624/// ```
625#[macro_export]
626macro_rules! register_schemas {
627    ($client:expr, $($schema:ty),+ $(,)?) => {
628        async {
629            $(
630                $client.register_schema::<$schema>().await;
631            )+
632        }
633    };
634}
635
636#[cfg(test)]
637mod macro_tests {
638    use super::*;
639
640    #[test]
641    fn test_expected_status_codes_single() {
642        let codes = expected_status_codes!(200);
643        assert!(codes.contains(200));
644        assert!(!codes.contains(201));
645    }
646
647    #[test]
648    fn test_expected_status_codes_multiple_single() {
649        let codes = expected_status_codes!(200, 201, 204);
650        assert!(codes.contains(200));
651        assert!(codes.contains(201));
652        assert!(codes.contains(204));
653        assert!(!codes.contains(202));
654    }
655
656    #[test]
657    fn test_expected_status_codes_range() {
658        let codes = expected_status_codes!(200 - 204);
659        assert!(codes.contains(200));
660        assert!(codes.contains(202));
661        assert!(codes.contains(204));
662        assert!(!codes.contains(205));
663    }
664
665    #[test]
666    fn test_expected_status_codes_mixed() {
667        let codes = expected_status_codes!(200, 201 - 204, 301, 400 - 404);
668        assert!(codes.contains(200));
669        assert!(codes.contains(202));
670        assert!(codes.contains(301));
671        assert!(codes.contains(402));
672        assert!(!codes.contains(305));
673    }
674
675    #[test]
676    fn test_expected_status_codes_trailing_comma() {
677        let codes = expected_status_codes!(200, 201,);
678        assert!(codes.contains(200));
679        assert!(codes.contains(201));
680    }
681
682    #[test]
683    fn test_expected_status_codes_range_trailing_comma() {
684        let codes = expected_status_codes!(200 - 204,);
685        assert!(codes.contains(202));
686    }
687
688    #[test]
689    fn test_expected_status_codes_five_elements() {
690        let codes = expected_status_codes!(200, 201, 202, 203, 204);
691        assert!(codes.contains(200));
692        assert!(codes.contains(201));
693        assert!(codes.contains(202));
694        assert!(codes.contains(203));
695        assert!(codes.contains(204));
696    }
697
698    #[test]
699    fn test_expected_status_codes_eight_elements() {
700        let codes = expected_status_codes!(200, 201, 202, 203, 204, 205, 206, 207);
701        assert!(codes.contains(200));
702        assert!(codes.contains(204));
703        assert!(codes.contains(207));
704    }
705
706    #[test]
707    fn test_expected_status_codes_multiple_ranges() {
708        let codes = expected_status_codes!(200 - 204, 300 - 304, 400 - 404);
709        assert!(codes.contains(202));
710        assert!(codes.contains(302));
711        assert!(codes.contains(402));
712        assert!(!codes.contains(205));
713        assert!(!codes.contains(305));
714    }
715
716    #[test]
717    fn test_expected_status_codes_edge_cases() {
718        // Empty should work
719        let _codes = expected_status_codes!();
720
721        // Single range should work
722        let codes = expected_status_codes!(200 - 299);
723        assert!(codes.contains(250));
724    }
725
726    #[test]
727    fn test_expected_status_codes_common_patterns() {
728        // Success codes
729        let success = expected_status_codes!(200 - 299);
730        assert!(success.contains(200));
731        assert!(success.contains(201));
732        assert!(success.contains(204));
733
734        // Client errors
735        let client_errors = expected_status_codes!(400 - 499);
736        assert!(client_errors.contains(400));
737        assert!(client_errors.contains(404));
738        assert!(client_errors.contains(422));
739
740        // Specific success codes
741        let specific = expected_status_codes!(200, 201, 204);
742        assert!(specific.contains(200));
743        assert!(!specific.contains(202));
744    }
745
746    #[test]
747    fn test_expected_status_codes_builder_alternative() {
748        // Using macro
749        let macro_codes = expected_status_codes!(200 - 204, 301, 302, 400 - 404);
750
751        // Using builder (should be equivalent)
752        let builder_codes = ExpectedStatusCodes::default()
753            .add_inclusive_range(200..=204)
754            .add_single(301)
755            .add_single(302)
756            .add_inclusive_range(400..=404);
757
758        // Both should have same results
759        for code in [200, 202, 204, 301, 302, 400, 402, 404] {
760            assert_eq!(macro_codes.contains(code), builder_codes.contains(code));
761        }
762    }
763}
764
765#[cfg(test)]
766mod integration_tests {
767    use super::*;
768
769    #[test]
770    fn test_expected_status_codes_real_world_patterns() {
771        // REST API common patterns
772        let rest_success = expected_status_codes!(200, 201, 204);
773        assert!(rest_success.contains(200)); // GET success
774        assert!(rest_success.contains(201)); // POST created
775        assert!(rest_success.contains(204)); // DELETE success
776
777        // GraphQL typically uses 200 for everything
778        let graphql = expected_status_codes!(200);
779        assert!(graphql.contains(200));
780        assert!(!graphql.contains(201));
781
782        // Health check endpoints
783        let health = expected_status_codes!(200, 503);
784        assert!(health.contains(200)); // Healthy
785        assert!(health.contains(503)); // Unhealthy
786
787        // Authentication endpoints
788        let auth = expected_status_codes!(200, 201, 401, 403);
789        assert!(auth.contains(200)); // Login success
790        assert!(auth.contains(401)); // Unauthorized
791        assert!(auth.contains(403)); // Forbidden
792    }
793
794    #[test]
795    fn test_expected_status_codes_with_api_call() {
796        // This tests that the macro works correctly with actual API calls
797        let client = ApiClient::builder().build().unwrap();
798        let codes = expected_status_codes!(200 - 299, 404);
799
800        // Should compile and be usable
801        let _call = client
802            .get("/test")
803            .unwrap()
804            .with_expected_status_codes(codes);
805    }
806
807    #[test]
808    fn test_expected_status_codes_method_chaining() {
809        let codes = expected_status_codes!(200)
810            .add_single(201)
811            .add_inclusive_range(300..=304);
812
813        assert!(codes.contains(200));
814        assert!(codes.contains(201));
815        assert!(codes.contains(302));
816    }
817
818    #[test]
819    fn test_expected_status_codes_vs_manual_creation() {
820        // Macro version
821        let macro_version = expected_status_codes!(200 - 204, 301, 400);
822
823        // Manual version
824        let manual_version = ExpectedStatusCodes::from_inclusive_range(200..=204)
825            .add_single(301)
826            .add_single(400);
827
828        // Should behave identically
829        for code in 100..600 {
830            assert_eq!(
831                macro_version.contains(code),
832                manual_version.contains(code),
833                "Mismatch for status code {code}"
834            );
835        }
836    }
837}