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}