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