Skip to main content

reinhardt_testkit/
lib.rs

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