html2pdf_api/factory/mock.rs
1//! Mock browser factory for testing.
2//!
3//! This module provides a mock implementation of [`BrowserFactory`] that
4//! can be configured to succeed or fail, useful for testing pool behavior
5//! without requiring Chrome to be installed.
6//!
7//! # Feature Flag
8//!
9//! This module is only available when:
10//! - The `test-utils` feature is enabled, OR
11//! - During testing (`#[cfg(test)]`)
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use html2pdf_api::factory::mock::MockBrowserFactory;
17//!
18//! // Factory that always fails
19//! let factory = MockBrowserFactory::always_fails("Chrome not installed");
20//!
21//! // Factory that fails after N successful creations
22//! let factory = MockBrowserFactory::fail_after_n(3, "Resource exhausted");
23//! ```
24
25use std::sync::Arc;
26use std::sync::atomic::{AtomicUsize, Ordering};
27
28use headless_chrome::Browser;
29
30use super::BrowserFactory;
31use crate::error::{BrowserPoolError, Result};
32
33/// Mock browser factory for testing without Chrome.
34///
35/// This factory can be configured to:
36/// - Always succeed (creates real browsers if Chrome available)
37/// - Always fail with a specific error
38/// - Fail after N successful creations
39/// - Track creation count for verification
40///
41/// # Thread Safety
42///
43/// This factory is `Send + Sync` and tracks state using atomic operations.
44///
45/// # Example
46///
47/// ```rust,ignore
48/// use html2pdf_api::factory::mock::MockBrowserFactory;
49///
50/// // Create a factory that always fails
51/// let factory = MockBrowserFactory::always_fails("Test error");
52/// assert!(factory.create().is_err());
53/// assert_eq!(factory.creation_count(), 1);
54///
55/// // Create a factory that fails after 2 successful creations
56/// let factory = MockBrowserFactory::fail_after_n(2, "Exhausted");
57/// ```
58pub struct MockBrowserFactory {
59 /// Whether to fail on creation.
60 should_fail: bool,
61
62 /// Custom error message when failing.
63 error_message: String,
64
65 /// Number of browsers created (for verification in tests).
66 creation_count: Arc<AtomicUsize>,
67
68 /// Optional: fail after this many successful creations.
69 fail_after: Option<usize>,
70}
71
72impl MockBrowserFactory {
73 /// Create a mock factory that attempts real browser creation.
74 ///
75 /// Note: This still requires Chrome to be installed to actually
76 /// create browsers. For pure mocking without Chrome, use
77 /// [`always_fails`](Self::always_fails).
78 ///
79 /// # Example
80 ///
81 /// ```rust,ignore
82 /// let factory = MockBrowserFactory::new();
83 /// // Will attempt real browser creation
84 /// let result = factory.create();
85 /// ```
86 pub fn new() -> Self {
87 Self {
88 should_fail: false,
89 error_message: String::new(),
90 creation_count: Arc::new(AtomicUsize::new(0)),
91 fail_after: None,
92 }
93 }
94
95 /// Create a mock factory that always fails with the given message.
96 ///
97 /// This is useful for testing error handling paths without
98 /// requiring Chrome to be installed.
99 ///
100 /// # Parameters
101 ///
102 /// * `message` - Error message to return on creation attempts.
103 ///
104 /// # Example
105 ///
106 /// ```rust,ignore
107 /// let factory = MockBrowserFactory::always_fails("Chrome not installed");
108 /// let result = factory.create();
109 /// assert!(result.is_err());
110 /// ```
111 pub fn always_fails<S: Into<String>>(message: S) -> Self {
112 Self {
113 should_fail: true,
114 error_message: message.into(),
115 creation_count: Arc::new(AtomicUsize::new(0)),
116 fail_after: None,
117 }
118 }
119
120 /// Create a mock factory that fails after N successful creations.
121 ///
122 /// Useful for testing pool behavior when browsers start failing
123 /// after some have been successfully created (e.g., resource exhaustion).
124 ///
125 /// # Parameters
126 ///
127 /// * `n` - Number of successful creations before failing.
128 /// * `message` - Error message after failures begin.
129 ///
130 /// # Example
131 ///
132 /// ```rust,ignore
133 /// let factory = MockBrowserFactory::fail_after_n(3, "Resource exhausted");
134 /// // First 3 calls may succeed (if Chrome installed), subsequent calls fail
135 /// ```
136 pub fn fail_after_n<S: Into<String>>(n: usize, message: S) -> Self {
137 Self {
138 should_fail: false,
139 error_message: message.into(),
140 creation_count: Arc::new(AtomicUsize::new(0)),
141 fail_after: Some(n),
142 }
143 }
144
145 /// Get the number of browser creation attempts by this factory.
146 ///
147 /// Useful for verifying pool behavior in tests.
148 ///
149 /// # Example
150 ///
151 /// ```rust,ignore
152 /// let factory = MockBrowserFactory::always_fails("test");
153 /// assert_eq!(factory.creation_count(), 0);
154 /// let _ = factory.create();
155 /// assert_eq!(factory.creation_count(), 1);
156 /// ```
157 pub fn creation_count(&self) -> usize {
158 self.creation_count.load(Ordering::SeqCst)
159 }
160
161 /// Reset the creation counter to zero.
162 ///
163 /// Useful when reusing a factory across multiple tests.
164 pub fn reset_count(&self) {
165 self.creation_count.store(0, Ordering::SeqCst);
166 }
167
168 /// Get a clone of the creation counter for external tracking.
169 ///
170 /// This allows test code to monitor creation count even after
171 /// the factory has been moved into a pool.
172 ///
173 /// # Example
174 ///
175 /// ```rust,ignore
176 /// let factory = MockBrowserFactory::new();
177 /// let counter = factory.counter();
178 ///
179 /// // Move factory into pool
180 /// let pool = BrowserPool::builder()
181 /// .factory(Box::new(factory))
182 /// .build()?;
183 ///
184 /// // Can still check count via cloned counter
185 /// println!("Created: {}", counter.load(Ordering::SeqCst));
186 /// ```
187 pub fn counter(&self) -> Arc<AtomicUsize> {
188 Arc::clone(&self.creation_count)
189 }
190}
191
192impl Default for MockBrowserFactory {
193 fn default() -> Self {
194 Self::new()
195 }
196}
197
198impl BrowserFactory for MockBrowserFactory {
199 /// Create a browser or return a mock error.
200 ///
201 /// Behavior depends on factory configuration:
202 /// - If `should_fail` is true, always returns error
203 /// - If `fail_after` is set and count exceeded, returns error
204 /// - Otherwise, attempts real browser creation
205 ///
206 /// # Errors
207 ///
208 /// Returns [`BrowserPoolError::BrowserCreation`] when configured to fail.
209 fn create(&self) -> Result<Browser> {
210 let count = self.creation_count.fetch_add(1, Ordering::SeqCst);
211
212 // Check if configured to always fail
213 if self.should_fail {
214 log::debug!("MockBrowserFactory: Returning configured failure");
215 return Err(BrowserPoolError::BrowserCreation(
216 self.error_message.clone(),
217 ));
218 }
219
220 // Check if we should fail after N creations
221 if let Some(fail_after) = self.fail_after {
222 if count >= fail_after {
223 log::debug!("MockBrowserFactory: Failing after {} creations", fail_after);
224 return Err(BrowserPoolError::BrowserCreation(
225 self.error_message.clone(),
226 ));
227 }
228 }
229
230 // Attempt real browser creation
231 log::debug!(
232 "MockBrowserFactory: Attempting real browser creation #{}",
233 count + 1
234 );
235
236 use super::chrome::create_chrome_options;
237
238 let options = create_chrome_options(None)
239 .map_err(|e| BrowserPoolError::Configuration(e.to_string()))?;
240
241 Browser::new(options).map_err(|e| {
242 log::error!("MockBrowserFactory: Real browser creation failed: {}", e);
243 BrowserPoolError::BrowserCreation(e.to_string())
244 })
245 }
246}
247
248impl std::fmt::Debug for MockBrowserFactory {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 f.debug_struct("MockBrowserFactory")
251 .field("should_fail", &self.should_fail)
252 .field("error_message", &self.error_message)
253 .field(
254 "creation_count",
255 &self.creation_count.load(Ordering::SeqCst),
256 )
257 .field("fail_after", &self.fail_after)
258 .finish()
259 }
260}
261
262// ============================================================================
263// Unit Tests
264// ============================================================================
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 /// Verifies that MockBrowserFactory can be created with different configurations.
271 #[test]
272 fn test_mock_factory_creation() {
273 let _factory = MockBrowserFactory::new();
274 let _factory = MockBrowserFactory::always_fails("test");
275 let _factory = MockBrowserFactory::fail_after_n(3, "exhausted");
276 }
277
278 /// Verifies that always_fails factory returns error.
279 #[test]
280 fn test_mock_factory_always_fails() {
281 let factory = MockBrowserFactory::always_fails("Test error");
282
283 let result = factory.create();
284 assert!(result.is_err());
285
286 match result {
287 Err(BrowserPoolError::BrowserCreation(msg)) => {
288 assert_eq!(msg, "Test error");
289 }
290 _ => panic!("Expected BrowserCreation error"),
291 }
292 }
293
294 /// Verifies that creation_count tracks attempts.
295 #[test]
296 fn test_mock_factory_creation_count() {
297 let factory = MockBrowserFactory::always_fails("Test");
298
299 assert_eq!(factory.creation_count(), 0);
300 let _ = factory.create();
301 assert_eq!(factory.creation_count(), 1);
302 let _ = factory.create();
303 assert_eq!(factory.creation_count(), 2);
304 }
305
306 /// Verifies that reset_count works.
307 #[test]
308 fn test_mock_factory_reset_count() {
309 let factory = MockBrowserFactory::always_fails("Test");
310
311 let _ = factory.create();
312 let _ = factory.create();
313 assert_eq!(factory.creation_count(), 2);
314
315 factory.reset_count();
316 assert_eq!(factory.creation_count(), 0);
317 }
318
319 /// Verifies that counter() returns a shared reference.
320 #[test]
321 fn test_mock_factory_counter() {
322 let factory = MockBrowserFactory::always_fails("Test");
323 let counter = factory.counter();
324
325 assert_eq!(counter.load(Ordering::SeqCst), 0);
326 let _ = factory.create();
327 assert_eq!(counter.load(Ordering::SeqCst), 1);
328 }
329
330 /// Verifies fail_after_n behavior.
331 #[test]
332 fn test_mock_factory_fail_after_n() {
333 let factory = MockBrowserFactory::fail_after_n(2, "Exhausted");
334
335 // First two attempts increment counter but may succeed or fail
336 // depending on Chrome availability - we just verify the count
337 let _ = factory.create();
338 let _ = factory.create();
339 assert_eq!(factory.creation_count(), 2);
340
341 // Third attempt should definitely fail with our message
342 let result = factory.create();
343 assert!(result.is_err());
344
345 if let Err(BrowserPoolError::BrowserCreation(msg)) = result {
346 assert_eq!(msg, "Exhausted");
347 }
348 }
349
350 /// Verifies Default implementation.
351 #[test]
352 fn test_mock_factory_default() {
353 let factory: MockBrowserFactory = Default::default();
354 assert_eq!(factory.creation_count(), 0);
355 assert!(!factory.should_fail);
356 }
357
358 /// Verifies Debug implementation.
359 #[test]
360 fn test_mock_factory_debug() {
361 let factory = MockBrowserFactory::always_fails("Test");
362 let debug_str = format!("{:?}", factory);
363
364 assert!(debug_str.contains("MockBrowserFactory"));
365 assert!(debug_str.contains("should_fail"));
366 assert!(debug_str.contains("true"));
367 }
368}