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}