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}