reinhardt_testkit/fixtures/loader.rs
1use rstest::*;
2use serde::Deserialize;
3use std::collections::HashMap;
4use std::sync::Arc;
5use tokio::sync::RwLock;
6
7/// Errors that can occur during fixture loading.
8#[derive(Debug, thiserror::Error)]
9pub enum FixtureError {
10 /// The requested fixture file was not found.
11 #[error("Fixture not found: {0}")]
12 NotFound(String),
13 /// An error occurred while reading the fixture file.
14 #[error("Load error: {0}")]
15 Load(String),
16 /// An error occurred while parsing the fixture content.
17 #[error("Parse error: {0}")]
18 Parse(String),
19}
20
21/// Result type for fixture loading operations.
22pub type FixtureResult<T> = Result<T, FixtureError>;
23
24/// Fixture data loader
25pub struct FixtureLoader {
26 fixtures: Arc<RwLock<HashMap<String, serde_json::Value>>>,
27}
28
29impl FixtureLoader {
30 /// Create a new fixture loader
31 ///
32 /// # Examples
33 ///
34 /// ```
35 /// use reinhardt_testkit::fixtures::FixtureLoader;
36 ///
37 /// let loader = FixtureLoader::new();
38 /// // Loader is ready to load fixtures
39 /// ```
40 pub fn new() -> Self {
41 Self {
42 fixtures: Arc::new(RwLock::new(HashMap::new())),
43 }
44 }
45 /// Load fixture from JSON string
46 ///
47 /// # Examples
48 ///
49 /// ```
50 /// use reinhardt_testkit::fixtures::FixtureLoader;
51 ///
52 /// # tokio_test::block_on(async {
53 /// let loader = FixtureLoader::new();
54 /// let json = r#"{"id": 1, "name": "Test"}"#;
55 /// loader.load_from_json("test".to_string(), json).await.unwrap();
56 /// assert!(loader.exists("test").await);
57 /// # });
58 /// ```
59 pub async fn load_from_json(&self, name: String, json: &str) -> FixtureResult<()> {
60 let value: serde_json::Value =
61 serde_json::from_str(json).map_err(|e| FixtureError::Parse(e.to_string()))?;
62
63 self.fixtures.write().await.insert(name, value);
64 Ok(())
65 }
66 /// Load fixture data
67 ///
68 /// # Examples
69 ///
70 /// ```
71 /// use reinhardt_testkit::fixtures::FixtureLoader;
72 /// use serde::Deserialize;
73 ///
74 /// #[derive(Deserialize)]
75 /// struct User {
76 /// id: i32,
77 /// name: String,
78 /// }
79 ///
80 /// # tokio_test::block_on(async {
81 /// let loader = FixtureLoader::new();
82 /// let json = r#"{"id": 1, "name": "Alice"}"#;
83 /// loader.load_from_json("user".to_string(), json).await.unwrap();
84 /// let user: User = loader.load("user").await.unwrap();
85 /// assert_eq!(user.id, 1);
86 /// assert_eq!(user.name, "Alice");
87 /// # });
88 /// ```
89 pub async fn load<T: for<'de> Deserialize<'de>>(&self, name: &str) -> FixtureResult<T> {
90 let fixtures = self.fixtures.read().await;
91 let value = fixtures
92 .get(name)
93 .ok_or_else(|| FixtureError::NotFound(name.to_string()))?;
94
95 serde_json::from_value(value.clone()).map_err(|e| FixtureError::Parse(e.to_string()))
96 }
97 /// Get raw fixture value
98 ///
99 /// # Examples
100 ///
101 /// ```
102 /// use reinhardt_testkit::fixtures::FixtureLoader;
103 ///
104 /// # tokio_test::block_on(async {
105 /// let loader = FixtureLoader::new();
106 /// let json = r#"{"status": "active"}"#;
107 /// loader.load_from_json("config".to_string(), json).await.unwrap();
108 /// let value = loader.get("config").await.unwrap();
109 /// assert!(value.is_object());
110 /// # });
111 /// ```
112 pub async fn get(&self, name: &str) -> FixtureResult<serde_json::Value> {
113 let fixtures = self.fixtures.read().await;
114 fixtures
115 .get(name)
116 .cloned()
117 .ok_or_else(|| FixtureError::NotFound(name.to_string()))
118 }
119 /// Check if fixture exists
120 ///
121 /// # Examples
122 ///
123 /// ```
124 /// use reinhardt_testkit::fixtures::FixtureLoader;
125 ///
126 /// # tokio_test::block_on(async {
127 /// let loader = FixtureLoader::new();
128 /// assert!(!loader.exists("missing").await);
129 /// loader.load_from_json("test".to_string(), "{}").await.unwrap();
130 /// assert!(loader.exists("test").await);
131 /// # });
132 /// ```
133 pub async fn exists(&self, name: &str) -> bool {
134 self.fixtures.read().await.contains_key(name)
135 }
136 /// Clear all fixtures
137 ///
138 /// # Examples
139 ///
140 /// ```
141 /// use reinhardt_testkit::fixtures::FixtureLoader;
142 ///
143 /// # tokio_test::block_on(async {
144 /// let loader = FixtureLoader::new();
145 /// loader.load_from_json("test".to_string(), "{}").await.unwrap();
146 /// assert_eq!(loader.list().await.len(), 1);
147 /// loader.clear().await;
148 /// assert_eq!(loader.list().await.len(), 0);
149 /// # });
150 /// ```
151 pub async fn clear(&self) {
152 self.fixtures.write().await.clear();
153 }
154 /// List all fixture names
155 ///
156 /// # Examples
157 ///
158 /// ```
159 /// use reinhardt_testkit::fixtures::FixtureLoader;
160 ///
161 /// # tokio_test::block_on(async {
162 /// let loader = FixtureLoader::new();
163 /// loader.load_from_json("test1".to_string(), "{}").await.unwrap();
164 /// loader.load_from_json("test2".to_string(), "{}").await.unwrap();
165 /// let names = loader.list().await;
166 /// assert_eq!(names.len(), 2);
167 /// assert!(names.contains(&"test1".to_string()));
168 /// # });
169 /// ```
170 pub async fn list(&self) -> Vec<String> {
171 self.fixtures.read().await.keys().cloned().collect()
172 }
173}
174
175impl Default for FixtureLoader {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181/// Factory trait for creating test data
182pub trait Factory<T>: Send + Sync {
183 /// Build a single instance of the test data type.
184 fn build(&self) -> T;
185 /// Build multiple instances of the test data type.
186 fn build_batch(&self, count: usize) -> Vec<T> {
187 (0..count).map(|_| self.build()).collect()
188 }
189}
190
191/// Simple factory builder
192pub struct FactoryBuilder<T, F>
193where
194 F: Fn() -> T + Send + Sync,
195{
196 builder: F,
197 _phantom: std::marker::PhantomData<T>,
198}
199
200impl<T, F> FactoryBuilder<T, F>
201where
202 F: Fn() -> T + Send + Sync,
203{
204 /// Create a new factory builder
205 ///
206 /// # Examples
207 ///
208 /// ```
209 /// use reinhardt_testkit::fixtures::{FactoryBuilder, Factory};
210 ///
211 /// #[derive(Debug, PartialEq)]
212 /// struct TestData { id: i32 }
213 ///
214 /// let factory = FactoryBuilder::new(|| TestData { id: 42 });
215 /// let item = factory.build();
216 /// assert_eq!(item.id, 42);
217 /// ```
218 pub fn new(builder: F) -> Self {
219 Self {
220 builder,
221 _phantom: std::marker::PhantomData,
222 }
223 }
224}
225
226impl<T, F> Factory<T> for FactoryBuilder<T, F>
227where
228 F: Fn() -> T + Send + Sync,
229 T: Send + Sync,
230{
231 fn build(&self) -> T {
232 (self.builder)()
233 }
234}
235
236/// Generate a random test key using UUID
237///
238/// # Examples
239///
240/// ```
241/// use reinhardt_testkit::fixtures::random_test_key;
242///
243/// let key = random_test_key();
244/// assert!(key.starts_with("test_key_"));
245/// ```
246pub fn random_test_key() -> String {
247 use uuid::Uuid;
248 format!("test_key_{}", Uuid::new_v4().simple())
249}
250
251/// Generate test configuration data with timestamp
252///
253/// # Examples
254///
255/// ```
256/// use reinhardt_testkit::fixtures::test_config_value;
257///
258/// let value = test_config_value("my_value");
259/// assert_eq!(value["value"], "my_value");
260/// ```
261pub fn test_config_value(value: &str) -> serde_json::Value {
262 serde_json::json!({
263 "value": value,
264 "timestamp": chrono::Utc::now().to_rfc3339(),
265 })
266}
267
268// ============================================================================
269// rstest integration: Fixtures for common test resources
270// ============================================================================
271
272/// Fixture providing a FixtureLoader instance
273///
274/// Use this fixture in tests that need to load JSON fixture data.
275///
276/// # Examples
277///
278/// ```rust,no_run
279/// use reinhardt_testkit::fixtures::fixture_loader;
280/// use rstest::*;
281///
282/// #[rstest]
283/// #[tokio::test]
284/// async fn test_with_fixtures(fixture_loader: reinhardt_testkit::fixtures::FixtureLoader) {
285/// fixture_loader.load_from_json("test".to_string(), r#"{"id": 1}"#).await.unwrap();
286/// // ...
287/// }
288/// ```
289#[fixture]
290pub fn fixture_loader() -> FixtureLoader {
291 FixtureLoader::new()
292}
293
294/// Fixture providing an APIClient instance
295///
296/// Use this fixture in tests that need to make test HTTP requests.
297///
298/// # Examples
299///
300/// ```rust,no_run
301/// use reinhardt_testkit::fixtures::api_client;
302/// use rstest::*;
303///
304/// #[rstest]
305/// #[tokio::test]
306/// async fn test_api_request(api_client: reinhardt_testkit::client::APIClient) {
307/// // Make requests with client
308/// }
309/// ```
310#[fixture]
311pub fn api_client() -> crate::client::APIClient {
312 crate::client::APIClient::new()
313}
314
315/// Fixture providing a temporary directory that is automatically cleaned up
316///
317/// # Examples
318///
319/// ```rust
320/// use reinhardt_testkit::fixtures::temp_dir;
321/// use rstest::*;
322///
323/// #[rstest]
324/// fn test_with_temp_dir(temp_dir: tempfile::TempDir) {
325/// let path = temp_dir.path();
326/// std::fs::write(path.join("test.txt"), "data").unwrap();
327/// // temp_dir is automatically cleaned up when test ends
328/// }
329/// ```
330#[fixture]
331pub fn temp_dir() -> tempfile::TempDir {
332 tempfile::tempdir().expect("Failed to create temporary directory")
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use serde::Serialize;
339
340 #[derive(Debug, Serialize, Deserialize, PartialEq)]
341 struct TestData {
342 id: i32,
343 name: String,
344 }
345
346 #[tokio::test]
347 async fn test_fixture_loader() {
348 let loader = FixtureLoader::new();
349 let json = r#"{"id": 1, "name": "Test"}"#;
350
351 loader
352 .load_from_json("test".to_string(), json)
353 .await
354 .unwrap();
355
356 let data: TestData = loader.load("test").await.unwrap();
357 assert_eq!(data.id, 1);
358 assert_eq!(data.name, "Test");
359 }
360
361 #[tokio::test]
362 async fn test_fixture_not_found() {
363 let loader = FixtureLoader::new();
364 let result: FixtureResult<TestData> = loader.load("missing").await;
365 assert!(result.is_err());
366 }
367
368 #[test]
369 fn test_factory_builder() {
370 let factory = FactoryBuilder::new(|| TestData {
371 id: 1,
372 name: "Test".to_string(),
373 });
374
375 let data = factory.build();
376 assert_eq!(data.id, 1);
377
378 let batch = factory.build_batch(3);
379 assert_eq!(batch.len(), 3);
380 }
381}