Skip to main content

reinhardt_test/
lib.rs

1//! # Reinhardt Test
2//!
3//! Testing utilities for the Reinhardt framework.
4//!
5//! ## Overview
6//!
7//! This crate provides comprehensive testing tools inspired by Django REST Framework,
8//! including API clients, request factories, assertions, and TestContainers integration
9//! for database testing.
10//!
11//! ## Features
12//!
13//! - **[`APIClient`]**: HTTP client for making test API requests
14//! - **[`APIRequestFactory`]**: Factory for creating mock HTTP requests
15//! - **[`APITestCase`]**: Base test case with common assertions
16//! - **Response Assertions**: Status, header, and body assertions
17//! - **[`Factory`]**: Model factory for generating test data
18//! - **[`DebugToolbar`]**: Debug panel for inspecting queries and timing
19//! - **[`WebSocketTestClient`]**: WebSocket connection testing
20//! - **TestContainers**: Database containers (PostgreSQL, MySQL, Redis) integration
21//!
22//! ## Quick Start
23//!
24//! ### API Client
25//!
26//! ```rust,ignore
27//! use reinhardt_test::{APIClient, assert_status};
28//! use hyper::StatusCode;
29//!
30//! #[tokio::test]
31//! async fn test_user_list() {
32//!     let client = APIClient::new("http://localhost:8000");
33//!
34//!     let response = client.get("/api/users/").await.unwrap();
35//!     assert_status(&response, StatusCode::OK);
36//!
37//!     let users: Vec<User> = response.json().await.unwrap();
38//!     assert!(!users.is_empty());
39//! }
40//! ```
41//!
42//! ### Request Factory
43//!
44//! ```rust,ignore
45//! use reinhardt_test::{APIRequestFactory, create_test_request};
46//!
47//! #[tokio::test]
48//! async fn test_view_directly() {
49//!     let factory = APIRequestFactory::new();
50//!
51//!     // Create a GET request
52//!     let request = factory.get("/api/users/").build();
53//!
54//!     // Create a POST request with JSON body
55//!     let request = factory.post("/api/users/")
56//!         .json(&json!({"name": "Alice"}))
57//!         .build();
58//!
59//!     // Pass to view handler directly
60//!     let response = my_view(request).await;
61//! }
62//! ```
63//!
64//! ### Assertions
65//!
66//! ```rust,ignore
67//! use reinhardt_test::{assert_status, assert_has_header, assert_header_equals, extract_json};
68//! use hyper::StatusCode;
69//!
70//! // Status assertions
71//! assert_status(&response, StatusCode::OK);
72//! assert_status(&response, StatusCode::CREATED);
73//!
74//! // Header assertions
75//! assert_has_header(&response, "Content-Type");
76//! assert_header_equals(&response, "Content-Type", "application/json");
77//!
78//! // Body extraction
79//! let data: MyStruct = extract_json(&response).await.unwrap();
80//! ```
81//!
82//! ### TestContainers (Database Testing)
83//!
84//! Requires the `testcontainers` feature:
85//!
86//! ```rust,ignore
87//! use reinhardt_test::{with_postgres, PostgresContainer};
88//!
89//! #[tokio::test]
90//! async fn test_with_database() {
91//!     with_postgres(|db: PostgresContainer| async move {
92//!         let connection_url = db.connection_url();
93//!
94//!         // Run tests against the database
95//!         let pool = create_pool(&connection_url).await;
96//!         // ...
97//!     }).await;
98//! }
99//! ```
100//!
101//! ### Model Factory
102//!
103//! ```rust,ignore
104//! use reinhardt_test::{Factory, FactoryBuilder};
105//!
106//! let user = FactoryBuilder::<User>::new()
107//!     .with("name", "Test User")
108//!     .with("email", "test@example.com")
109//!     .build();
110//! ```
111//!
112//! ## Modules
113//!
114//! - [`assertions`]: Response assertion utilities
115//! - [`client`]: [`APIClient`] for HTTP testing
116//! - [`factory`]: [`APIRequestFactory`] for request creation
117//! - [`fixtures`]: Test data generation and fixtures
118//! - [`http`]: HTTP helper functions
119//! - [`mock`]: Mock objects and spies
120//! - [`server`]: Test server utilities
121//! - [`testcase`]: [`APITestCase`] base class
122//! - `containers`: TestContainers integration (requires feature)
123//!
124//! ## Feature Flags
125//!
126//! - **`testcontainers`**: Enable TestContainers for database testing
127//! - **`static`**: Enable static file testing utilities
128//! - **`wasm`**: Enable WASM frontend testing utilities
129//! - **`wasm-full`**: Enable WASM testing with full web-sys features
130//! - **`server-fn-test`**: Enable server function testing utilities
131
132pub mod assertions;
133pub mod client;
134pub mod debug;
135pub mod factory;
136pub mod fixtures;
137pub mod http;
138pub mod logging;
139pub mod messages;
140pub mod mock;
141pub mod resource;
142pub mod response;
143pub mod server;
144pub mod testcase;
145pub mod views;
146pub mod viewsets;
147
148#[cfg(feature = "testcontainers")]
149pub mod containers;
150
151pub mod websocket;
152
153// Re-export testcontainers crates for convenient access via reinhardt::test::testcontainers
154#[cfg(feature = "testcontainers")]
155pub use testcontainers;
156
157#[cfg(feature = "testcontainers")]
158pub use testcontainers_modules;
159
160#[cfg(feature = "static")]
161pub mod static_files;
162
163#[cfg(feature = "wasm")]
164pub mod wasm;
165
166#[cfg(feature = "server-fn-test")]
167pub mod server_fn;
168
169// Re-exports for impl_test_model! macro
170#[doc(hidden)]
171pub use paste;
172#[doc(hidden)]
173pub use reinhardt_db::orm::inspection;
174#[doc(hidden)]
175pub use reinhardt_db::orm::relationship;
176#[doc(hidden)]
177pub use reinhardt_db::orm::{FieldSelector, Model};
178
179pub use assertions::*;
180pub use client::{APIClient, APIClientBuilder, ClientError, HttpVersion};
181pub use debug::{DebugEntry, DebugPanel, DebugToolbar, SqlQuery, TimingInfo};
182pub use factory::{APIRequestFactory, RequestBuilder};
183pub use fixtures::{
184	Factory, FactoryBuilder, FixtureError, FixtureLoader, FixtureResult, api_client_from_url,
185	random_test_key, test_config_value, test_server_guard,
186};
187
188// Re-export commonly used types for testing
189pub use reinhardt_urls::routers::ServerRouter;
190
191#[cfg(feature = "testcontainers")]
192pub use fixtures::{postgres_container, redis_container};
193pub use http::{
194	assert_has_header, assert_header_contains, assert_header_equals, assert_no_header,
195	assert_status, create_insecure_request, create_request, create_response_with_headers,
196	create_response_with_status, create_secure_request, create_test_request, create_test_response,
197	extract_json, get_header, has_header, header_contains, header_equals,
198};
199pub use logging::init_test_logging;
200pub use messages::{
201	MessagesTestMixin, assert_message_count, assert_message_exists, assert_message_level,
202	assert_message_tags, assert_messages,
203};
204pub use mock::{CallRecord, MockFunction, SimpleHandler, Spy};
205pub use resource::{
206	AsyncTeardownGuard, AsyncTestResource, SuiteGuard, SuiteResource, TeardownGuard, TestResource,
207	acquire_suite,
208};
209pub use response::{ResponseExt, TestResponse};
210pub use server::{
211	BodyEchoHandler, DelayedHandler, EchoPathHandler, LargeResponseHandler, MethodEchoHandler,
212	RouterHandler, StatusCodeHandler, shutdown_test_server, spawn_test_server,
213};
214pub use testcase::APITestCase;
215pub use views::{
216	ApiTestModel, ErrorKind, ErrorTestView, SimpleTestView, TestModel, create_api_test_objects,
217	create_json_request, create_large_test_objects, create_request as create_view_request,
218	create_request_with_headers, create_request_with_path_params, create_test_objects,
219};
220pub use viewsets::{SimpleViewSet, TestViewSet};
221
222#[cfg(feature = "testcontainers")]
223pub use containers::{
224	MailpitContainer, MySqlContainer, PostgresContainer, RabbitMQContainer, RedisContainer,
225	TestDatabase, with_mailpit, with_mysql, with_postgres, with_rabbitmq, with_redis,
226};
227
228#[cfg(feature = "static")]
229pub use static_files::*;
230
231pub use websocket::WebSocketTestClient;
232
233/// Re-export commonly used testing types
234pub mod prelude {
235	pub use super::assertions::*;
236	pub use super::client::APIClient;
237	pub use super::debug::DebugToolbar;
238	pub use super::factory::APIRequestFactory;
239	pub use super::fixtures::{
240		Factory, FactoryBuilder, FixtureLoader, api_client_from_url, random_test_key,
241		test_config_value,
242	};
243
244	#[cfg(feature = "testcontainers")]
245	pub use super::fixtures::{postgres_container, redis_container};
246	pub use super::http::{
247		assert_has_header, assert_header_contains, assert_header_equals, assert_no_header,
248		assert_status, create_insecure_request, create_request, create_response_with_headers,
249		create_response_with_status, create_secure_request, create_test_request,
250		create_test_response, extract_json, get_header, has_header, header_contains, header_equals,
251	};
252	pub use super::logging::init_test_logging;
253	pub use super::messages::{
254		MessagesTestMixin, assert_message_count, assert_message_exists, assert_messages,
255	};
256	pub use super::mock::{MockFunction, SimpleHandler, Spy};
257	pub use super::poll_until;
258	pub use super::resource::{
259		AsyncTeardownGuard, AsyncTestResource, SuiteGuard, SuiteResource, TeardownGuard,
260		TestResource, acquire_suite,
261	};
262	pub use super::response::TestResponse;
263	pub use super::server::{
264		BodyEchoHandler, DelayedHandler, EchoPathHandler, LargeResponseHandler, MethodEchoHandler,
265		RouterHandler, StatusCodeHandler, shutdown_test_server, spawn_test_server,
266	};
267	#[cfg(feature = "testcontainers")]
268	pub use super::testcase::TransactionHandle;
269	pub use super::testcase::{APITestCase, TeardownError};
270	pub use super::views::{
271		ApiTestModel, ErrorTestView, SimpleTestView, TestModel, create_api_test_objects,
272		create_test_objects,
273	};
274	pub use super::viewsets::{SimpleViewSet, TestViewSet};
275
276	#[cfg(feature = "testcontainers")]
277	pub use super::containers::{
278		MySqlContainer, PostgresContainer, RedisContainer, TestDatabase, with_mysql, with_postgres,
279		with_redis,
280	};
281
282	#[cfg(feature = "static")]
283	pub use super::static_files::*;
284}
285
286/// Poll a condition until it becomes true or timeout is reached.
287///
288/// This is useful for testing asynchronous operations that may take some time to complete,
289/// such as cache expiration, rate limit window resets, or background task completion.
290///
291/// # Arguments
292///
293/// * `timeout` - Maximum duration to wait for the condition to become true
294/// * `interval` - Duration to wait between each poll attempt
295/// * `condition` - Async closure that returns `true` when the desired state is reached
296///
297/// # Returns
298///
299/// * `Ok(())` if the condition becomes true within the timeout
300/// * `Err(String)` if the timeout is reached before the condition becomes true
301///
302/// # Examples
303///
304/// ```no_run
305/// use reinhardt_test::poll_until;
306/// use std::time::Duration;
307///
308/// # async fn example() {
309/// // Poll until a cache entry expires
310/// poll_until(
311///     Duration::from_millis(200),
312///     Duration::from_millis(10),
313///     || async {
314///         // Check if cache entry has expired
315///         // cache.get("key").await.is_none()
316///         true
317///     }
318/// ).await.expect("Condition should be met");
319/// # }
320/// ```
321pub async fn poll_until<F, Fut>(
322	timeout: std::time::Duration,
323	interval: std::time::Duration,
324	mut condition: F,
325) -> Result<(), String>
326where
327	F: FnMut() -> Fut,
328	Fut: std::future::Future<Output = bool>,
329{
330	let start = std::time::Instant::now();
331	while start.elapsed() < timeout {
332		if condition().await {
333			return Ok(());
334		}
335		tokio::time::sleep(interval).await;
336	}
337	Err(format!("Timeout after {:?} waiting for condition", timeout))
338}
339
340/// Helper macro for implementing Model trait with empty Fields for test models
341///
342/// This macro generates the boilerplate code needed for test models that don't use
343/// the full `#[model(...)]` macro. It creates an empty field selector struct and
344/// implements the required Model trait methods.
345///
346/// # Usage
347///
348/// ```ignore
349/// #[derive(Debug, Clone, Serialize, Deserialize)]
350/// struct TestUser {
351///     id: Option<i64>,
352///     name: String,
353/// }
354///
355/// impl_test_model!(TestUser, i64, "test_users");
356/// ```
357///
358/// This expands to:
359/// - A `TestUserFields` struct that implements `FieldSelector`
360/// - A complete `Model` trait implementation for `TestUser`
361///
362/// # Parameters
363///
364/// - `$model`: The model struct name
365/// - `$pk`: The primary key type
366/// - `$table`: The table name as a string literal
367/// - `$app`: The application label as a string literal (optional, defaults to "default")
368/// - `relationships`: Optional relationship definitions (see examples below)
369///
370/// # Constraints
371/// - Model must have an `id: Option<PrimaryKey>` field
372/// - Primary key field name is fixed to `"id"`
373///
374/// # Examples
375///
376/// ## Basic usage
377/// ```ignore
378/// #[derive(Debug, Clone, Serialize, Deserialize)]
379/// struct User {
380///     id: Option<i64>,
381///     name: String,
382/// }
383///
384/// // With app_label
385/// reinhardt_test::impl_test_model!(User, i64, "users", "auth");
386///
387/// // Without app_label (defaults to "default")
388/// reinhardt_test::impl_test_model!(Product, i32, "products");
389/// ```
390///
391/// ## With relationships
392/// ```ignore
393/// #[derive(Debug, Clone, Serialize, Deserialize)]
394/// struct Author {
395///     id: Option<i32>,
396///     name: String,
397/// }
398///
399/// // OneToMany relationship
400/// reinhardt_test::impl_test_model!(
401///     Author, i32, "authors", "test",
402///     relationships: [
403///         (OneToMany, "books", "Book", "author_id", "author")
404///     ]
405/// );
406///
407/// // Multiple relationships
408/// #[derive(Debug, Clone, Serialize, Deserialize)]
409/// struct Book {
410///     id: Option<i32>,
411///     title: String,
412///     author_id: i32,
413///     publisher_id: i32,
414/// }
415///
416/// reinhardt_test::impl_test_model!(
417///     Book, i32, "books", "test",
418///     relationships: [
419///         (ManyToOne, "author", "Author", "author_id", "books"),
420///         (ManyToOne, "publisher", "Publisher", "publisher_id", "books"),
421///         (OneToMany, "reviews", "Review", "book_id", "book")
422///     ]
423/// );
424/// ```
425#[macro_export]
426macro_rules! impl_test_model {
427	// Composite version (OneToMany/ManyToOne + ManyToMany) - HIGHEST PRIORITY MATCHING
428	(
429		$model:ident,
430		$pk:ty,
431		$table:expr,
432		$app:expr,
433		relationships: [
434			$(($rel_type:ident, $rel_name:expr, $related:expr, $fk:expr, $back_pop:expr)),* $(,)?
435		],
436		many_to_many: [
437			$(($m2m_name:expr, $m2m_related:expr, $m2m_through:expr, $m2m_source:expr, $m2m_target:expr)),* $(,)?
438		]
439	) => {
440		$crate::paste::paste! {
441			#[derive(Debug, Clone)]
442			pub struct [<$model Fields>];
443
444			impl $crate::FieldSelector for [<$model Fields>] {
445				fn with_alias(self, _alias: &str) -> Self {
446					self
447				}
448			}
449
450			impl $crate::Model for $model {
451				type PrimaryKey = $pk;
452				type Fields = [<$model Fields>];
453
454				fn table_name() -> &'static str {
455					$table
456				}
457
458				fn app_label() -> &'static str {
459					$app
460				}
461
462				fn primary_key(&self) -> Option<Self::PrimaryKey> {
463					self.id
464				}
465
466				fn set_primary_key(&mut self, value: Self::PrimaryKey) {
467					self.id = Some(value);
468				}
469
470				fn primary_key_field() -> &'static str {
471					"id"
472				}
473
474				fn new_fields() -> Self::Fields {
475					[<$model Fields>]
476				}
477
478				fn relationship_metadata() -> Vec<$crate::inspection::RelationInfo> {
479					// OneToMany/ManyToOne/OneToOne relationships
480					let mut relations = vec![
481						$(
482							$crate::inspection::RelationInfo {
483								name: $rel_name.to_string(),
484								relationship_type: $crate::relationship::RelationshipType::$rel_type,
485								related_model: $related.to_string(),
486								foreign_key: Some($fk.to_string()),
487								back_populates: Some($back_pop.to_string()),
488								through_table: None,
489								source_field: None,
490								target_field: None,
491							}
492						),*
493					];
494
495					// ManyToMany relationships
496					relations.extend(vec![
497						$(
498							$crate::inspection::RelationInfo {
499								name: $m2m_name.to_string(),
500								relationship_type: $crate::relationship::RelationshipType::ManyToMany,
501								related_model: $m2m_related.to_string(),
502								foreign_key: None,
503								back_populates: None,
504								through_table: Some($m2m_through.to_string()),
505								source_field: Some($m2m_source.to_string()),
506								target_field: Some($m2m_target.to_string()),
507							}
508						),*
509					]);
510
511					relations
512				}
513			}
514		}
515	};
516
517	// Version with relationships (OneToMany/ManyToOne/OneToOne)
518	(
519		$model:ident,
520		$pk:ty,
521		$table:expr,
522		$app:expr,
523		relationships: [
524			$(($rel_type:ident, $rel_name:expr, $related:expr, $fk:expr, $back_pop:expr)),* $(,)?
525		]
526	) => {
527		$crate::paste::paste! {
528			#[derive(Debug, Clone)]
529			pub struct [<$model Fields>];
530
531			impl $crate::FieldSelector for [<$model Fields>] {
532				fn with_alias(self, _alias: &str) -> Self {
533					self
534				}
535			}
536
537			impl $crate::Model for $model {
538				type PrimaryKey = $pk;
539				type Fields = [<$model Fields>];
540
541				fn table_name() -> &'static str {
542					$table
543				}
544
545				fn app_label() -> &'static str {
546					$app
547				}
548
549				fn primary_key(&self) -> Option<Self::PrimaryKey> {
550					self.id
551				}
552
553				fn set_primary_key(&mut self, value: Self::PrimaryKey) {
554					self.id = Some(value);
555				}
556
557				fn primary_key_field() -> &'static str {
558					"id"
559				}
560
561				fn new_fields() -> Self::Fields {
562					[<$model Fields>]
563				}
564
565				fn relationship_metadata() -> Vec<$crate::inspection::RelationInfo> {
566					vec![
567						$(
568							$crate::inspection::RelationInfo {
569								name: $rel_name.to_string(),
570								relationship_type: $crate::relationship::RelationshipType::$rel_type,
571								related_model: $related.to_string(),
572								foreign_key: Some($fk.to_string()),
573								back_populates: Some($back_pop.to_string()),
574								through_table: None,
575								source_field: None,
576								target_field: None,
577							}
578						),*
579					]
580				}
581			}
582		}
583	};
584
585	// ManyToMany only version
586	(
587		$model:ident,
588		$pk:ty,
589		$table:expr,
590		$app:expr,
591		many_to_many: [
592			$(($rel_name:expr, $related:expr, $through:expr, $source:expr, $target:expr)),* $(,)?
593		]
594	) => {
595		$crate::paste::paste! {
596			#[derive(Debug, Clone)]
597			pub struct [<$model Fields>];
598
599			impl $crate::FieldSelector for [<$model Fields>] {
600				fn with_alias(self, _alias: &str) -> Self {
601					self
602				}
603			}
604
605			impl $crate::Model for $model {
606				type PrimaryKey = $pk;
607				type Fields = [<$model Fields>];
608
609				fn table_name() -> &'static str {
610					$table
611				}
612
613				fn app_label() -> &'static str {
614					$app
615				}
616
617				fn primary_key(&self) -> Option<Self::PrimaryKey> {
618					self.id
619				}
620
621				fn set_primary_key(&mut self, value: Self::PrimaryKey) {
622					self.id = Some(value);
623				}
624
625				fn primary_key_field() -> &'static str {
626					"id"
627				}
628
629				fn new_fields() -> Self::Fields {
630					[<$model Fields>]
631				}
632
633				fn relationship_metadata() -> Vec<$crate::inspection::RelationInfo> {
634					vec![
635						$(
636							$crate::inspection::RelationInfo {
637								name: $rel_name.to_string(),
638								relationship_type: $crate::relationship::RelationshipType::ManyToMany,
639								related_model: $related.to_string(),
640								foreign_key: None,
641								back_populates: None,
642								through_table: Some($through.to_string()),
643								source_field: Some($source.to_string()),
644								target_field: Some($target.to_string()),
645							}
646						),*
647					]
648				}
649			}
650		}
651	};
652
653	// Version with app_label (no relationships)
654	($model:ident, $pk:ty, $table:expr, $app:expr) => {
655		$crate::paste::paste! {
656			#[derive(Debug, Clone)]
657			pub struct [<$model Fields>];
658
659			impl $crate::FieldSelector for [<$model Fields>] {
660				fn with_alias(self, _alias: &str) -> Self {
661					self
662				}
663			}
664
665			impl $crate::Model for $model {
666				type PrimaryKey = $pk;
667				type Fields = [<$model Fields>];
668
669				fn table_name() -> &'static str {
670					$table
671				}
672
673				fn app_label() -> &'static str {
674					$app
675				}
676
677				fn primary_key(&self) -> Option<Self::PrimaryKey> {
678					self.id
679				}
680
681				fn set_primary_key(&mut self, value: Self::PrimaryKey) {
682					self.id = Some(value);
683				}
684
685				fn primary_key_field() -> &'static str {
686					"id"
687				}
688
689				fn new_fields() -> Self::Fields {
690					[<$model Fields>]
691				}
692			}
693		}
694	};
695
696	// Backward compatibility: default app_label
697	($model:ident, $pk:ty, $table:expr) => {
698		$crate::impl_test_model!($model, $pk, $table, "default");
699	};
700
701	// Non-option primary key version with app_label
702	// Use this when the primary key field is NOT wrapped in Option<T>
703	// Example: id: Uuid instead of id: Option<Uuid>
704	($model:ident, $pk:ty, $table:expr, $app:expr, non_option_pk) => {
705		$crate::paste::paste! {
706			#[derive(Debug, Clone)]
707			pub struct [<$model Fields>];
708
709			impl $crate::FieldSelector for [<$model Fields>] {
710				fn with_alias(self, _alias: &str) -> Self {
711					self
712				}
713			}
714
715			impl $crate::Model for $model {
716				type PrimaryKey = $pk;
717				type Fields = [<$model Fields>];
718
719				fn table_name() -> &'static str {
720					$table
721				}
722
723				fn app_label() -> &'static str {
724					$app
725				}
726
727				fn primary_key(&self) -> Option<Self::PrimaryKey> {
728					Some(self.id)
729				}
730
731				fn set_primary_key(&mut self, value: Self::PrimaryKey) {
732					self.id = value;
733				}
734
735				fn primary_key_field() -> &'static str {
736					"id"
737				}
738
739				fn new_fields() -> Self::Fields {
740					[<$model Fields>]
741				}
742			}
743		}
744	};
745
746	// Non-option primary key version with default app_label
747	($model:ident, $pk:ty, $table:expr, non_option_pk) => {
748		$crate::impl_test_model!($model, $pk, $table, "default", non_option_pk);
749	};
750}