mockforge_plugin_sdk/
testing.rs

1//! Testing utilities for plugin development
2//!
3//! This module provides test harnesses, mock contexts, and utilities
4//! for testing plugins in isolation.
5
6use mockforge_plugin_core::*;
7use std::collections::HashMap;
8
9/// Test harness for plugin development
10///
11/// Provides a mock environment for testing plugins without
12/// loading them into the actual plugin loader.
13///
14/// # Example
15///
16/// ```rust
17/// use mockforge_plugin_sdk::testing::TestHarness;
18/// use mockforge_plugin_sdk::prelude::*;
19///
20/// #[tokio::test]
21/// async fn test_my_plugin() {
22///     let harness = TestHarness::new();
23///     let context = harness.create_context("test-plugin", "req-123");
24///
25///     // Test your plugin here
26/// }
27/// ```
28pub struct TestHarness {
29    /// Mock plugin contexts
30    contexts: HashMap<String, PluginContext>,
31}
32
33impl TestHarness {
34    /// Create a new test harness
35    pub fn new() -> Self {
36        Self {
37            contexts: HashMap::new(),
38        }
39    }
40
41    /// Create a mock plugin context
42    pub fn create_context(&mut self, plugin_id: &str, request_id: &str) -> PluginContext {
43        let mut context = PluginContext::new(PluginId::new(plugin_id), PluginVersion::new(0, 1, 0));
44
45        // Override request_id if provided
46        context.request_id = request_id.to_string();
47
48        self.contexts.insert(plugin_id.to_string(), context.clone());
49        context
50    }
51
52    /// Create a context with custom data
53    pub fn create_context_with_custom(
54        &mut self,
55        plugin_id: &str,
56        request_id: &str,
57        custom_data: HashMap<String, serde_json::Value>,
58    ) -> PluginContext {
59        let mut context = self.create_context(plugin_id, request_id);
60        for (key, value) in custom_data {
61            context = context.with_custom(key, value);
62        }
63        context
64    }
65
66    /// Get a context by plugin ID
67    pub fn get_context(&self, plugin_id: &str) -> Option<&PluginContext> {
68        self.contexts.get(plugin_id)
69    }
70}
71
72impl Default for TestHarness {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78/// Mock authentication request helpers for testing
79pub struct MockAuthRequest;
80
81impl MockAuthRequest {
82    /// Create a mock auth request with basic auth header
83    pub fn with_basic_auth(username: &str, password: &str) -> AuthRequest {
84        use axum::http::{HeaderMap, HeaderValue, Method, Uri};
85        use base64::Engine;
86
87        let credentials = format!("{}:{}", username, password);
88        let encoded = base64::engine::general_purpose::STANDARD.encode(credentials.as_bytes());
89        let auth_value = format!("Basic {}", encoded);
90
91        let mut headers = HeaderMap::new();
92        headers.insert("authorization", HeaderValue::from_str(&auth_value).unwrap());
93
94        AuthRequest::from_axum(Method::GET, Uri::from_static("/"), headers, None)
95    }
96
97    /// Create a mock auth request with bearer token
98    pub fn with_bearer_token(token: &str) -> AuthRequest {
99        use axum::http::{HeaderMap, HeaderValue, Method, Uri};
100
101        let auth_value = format!("Bearer {}", token);
102
103        let mut headers = HeaderMap::new();
104        headers.insert("authorization", HeaderValue::from_str(&auth_value).unwrap());
105
106        AuthRequest::from_axum(Method::GET, Uri::from_static("/"), headers, None)
107    }
108
109    /// Create a mock auth request with custom headers
110    pub fn with_headers(headers_map: HashMap<String, String>) -> AuthRequest {
111        use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, Uri};
112
113        let mut headers = HeaderMap::new();
114        for (key, value) in headers_map {
115            if let (Ok(header_name), Ok(header_value)) =
116                (key.parse::<HeaderName>(), HeaderValue::from_str(&value))
117            {
118                headers.insert(header_name, header_value);
119            }
120        }
121
122        AuthRequest::from_axum(Method::GET, Uri::from_static("/"), headers, None)
123    }
124}
125
126/// Assert that a plugin result is successful
127#[macro_export]
128macro_rules! assert_plugin_ok {
129    ($result:expr) => {
130        match $result {
131            Ok(_) => (),
132            Err(e) => panic!("Plugin returned error: {:?}", e),
133        }
134    };
135    ($result:expr, $msg:expr) => {
136        match $result {
137            Ok(_) => (),
138            Err(e) => panic!("{}: {:?}", $msg, e),
139        }
140    };
141}
142
143/// Assert that a plugin result is an error
144#[macro_export]
145macro_rules! assert_plugin_err {
146    ($result:expr) => {
147        match $result {
148            Ok(_) => panic!("Expected plugin error, got success"),
149            Err(_) => (),
150        }
151    };
152    ($result:expr, $msg:expr) => {
153        match $result {
154            Ok(_) => panic!("{}: Expected error, got success", $msg),
155            Err(_) => (),
156        }
157    };
158}
159
160/// Create a test plugin context
161pub fn test_context() -> PluginContext {
162    let mut context = PluginContext::new(PluginId::new("test-plugin"), PluginVersion::new(0, 1, 0));
163    context.request_id = "test-request".to_string();
164    context
165}
166
167/// Create a test plugin context with custom ID
168pub fn test_context_with_id(plugin_id: &str) -> PluginContext {
169    let mut context = PluginContext::new(PluginId::new(plugin_id), PluginVersion::new(0, 1, 0));
170    context.request_id = "test-request".to_string();
171    context
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_harness_creation() {
180        let harness = TestHarness::new();
181        assert_eq!(harness.contexts.len(), 0);
182    }
183
184    #[test]
185    fn test_context_creation() {
186        let mut harness = TestHarness::new();
187        let context = harness.create_context("test", "req-1");
188        assert_eq!(context.plugin_id.as_str(), "test");
189        assert_eq!(context.request_id, "req-1");
190    }
191
192    #[test]
193    fn test_mock_auth_request() {
194        let request = MockAuthRequest::with_basic_auth("user", "pass");
195        let auth_header = request.authorization_header();
196        assert!(auth_header.is_some());
197        assert!(auth_header.unwrap().starts_with("Basic "));
198
199        let (username, password) = request.basic_credentials().unwrap();
200        assert_eq!(username, "user");
201        assert_eq!(password, "pass");
202    }
203
204    #[test]
205    fn test_harness_default() {
206        let harness = TestHarness::default();
207        assert_eq!(harness.contexts.len(), 0);
208    }
209
210    #[test]
211    fn test_harness_multiple_contexts() {
212        let mut harness = TestHarness::new();
213        harness.create_context("plugin-1", "req-1");
214        harness.create_context("plugin-2", "req-2");
215        harness.create_context("plugin-3", "req-3");
216        assert_eq!(harness.contexts.len(), 3);
217    }
218
219    #[test]
220    fn test_harness_get_context() {
221        let mut harness = TestHarness::new();
222        harness.create_context("my-plugin", "req-123");
223
224        let retrieved = harness.get_context("my-plugin");
225        assert!(retrieved.is_some());
226        let ctx = retrieved.unwrap();
227        assert_eq!(ctx.plugin_id.as_str(), "my-plugin");
228        assert_eq!(ctx.request_id, "req-123");
229    }
230
231    #[test]
232    fn test_harness_get_context_not_found() {
233        let harness = TestHarness::new();
234        assert!(harness.get_context("nonexistent").is_none());
235    }
236
237    #[test]
238    fn test_harness_context_overwrite() {
239        let mut harness = TestHarness::new();
240        harness.create_context("plugin", "req-1");
241        harness.create_context("plugin", "req-2");
242
243        // Should have only 1 context (overwritten)
244        assert_eq!(harness.contexts.len(), 1);
245        let ctx = harness.get_context("plugin").unwrap();
246        assert_eq!(ctx.request_id, "req-2");
247    }
248
249    #[test]
250    fn test_harness_create_context_with_custom() {
251        let mut harness = TestHarness::new();
252        let mut custom_data = HashMap::new();
253        custom_data.insert("key1".to_string(), serde_json::json!("value1"));
254        custom_data.insert("key2".to_string(), serde_json::json!(42));
255
256        let context = harness.create_context_with_custom("plugin", "req-1", custom_data);
257        assert_eq!(context.plugin_id.as_str(), "plugin");
258        assert_eq!(context.request_id, "req-1");
259    }
260
261    #[test]
262    fn test_harness_create_context_with_empty_custom() {
263        let mut harness = TestHarness::new();
264        let context = harness.create_context_with_custom("plugin", "req-1", HashMap::new());
265        assert_eq!(context.plugin_id.as_str(), "plugin");
266    }
267
268    #[test]
269    fn test_mock_auth_request_bearer_token() {
270        let request = MockAuthRequest::with_bearer_token("my-secret-token");
271        let auth_header = request.authorization_header();
272        assert!(auth_header.is_some());
273        let header = auth_header.unwrap();
274        assert!(header.starts_with("Bearer "));
275        assert!(header.contains("my-secret-token"));
276    }
277
278    #[test]
279    fn test_mock_auth_request_with_headers() {
280        let mut headers = HashMap::new();
281        headers.insert("x-custom-header".to_string(), "custom-value".to_string());
282        headers.insert("content-type".to_string(), "application/json".to_string());
283
284        let request = MockAuthRequest::with_headers(headers);
285        // The request should be created successfully
286        assert_eq!(request.method, "GET");
287    }
288
289    #[test]
290    fn test_mock_auth_request_with_empty_headers() {
291        let request = MockAuthRequest::with_headers(HashMap::new());
292        assert_eq!(request.method, "GET");
293        assert!(request.authorization_header().is_none());
294    }
295
296    #[test]
297    fn test_mock_auth_request_basic_auth_empty_credentials() {
298        let request = MockAuthRequest::with_basic_auth("", "");
299        let (username, password) = request.basic_credentials().unwrap();
300        assert_eq!(username, "");
301        assert_eq!(password, "");
302    }
303
304    #[test]
305    fn test_mock_auth_request_bearer_empty_token() {
306        let request = MockAuthRequest::with_bearer_token("");
307        let auth_header = request.authorization_header();
308        assert!(auth_header.is_some());
309        assert_eq!(auth_header.unwrap(), "Bearer ");
310    }
311
312    #[test]
313    fn test_test_context_function() {
314        let context = test_context();
315        assert_eq!(context.plugin_id.as_str(), "test-plugin");
316        assert_eq!(context.request_id, "test-request");
317        assert_eq!(context.version.major, 0);
318        assert_eq!(context.version.minor, 1);
319        assert_eq!(context.version.patch, 0);
320    }
321
322    #[test]
323    fn test_test_context_with_id_function() {
324        let context = test_context_with_id("custom-plugin");
325        assert_eq!(context.plugin_id.as_str(), "custom-plugin");
326        assert_eq!(context.request_id, "test-request");
327    }
328
329    #[test]
330    fn test_test_context_with_empty_id() {
331        let context = test_context_with_id("");
332        assert_eq!(context.plugin_id.as_str(), "");
333        assert_eq!(context.request_id, "test-request");
334    }
335
336    #[test]
337    fn test_assert_plugin_ok_macro() {
338        let result: std::result::Result<i32, &str> = Ok(42);
339        assert_plugin_ok!(result);
340    }
341
342    #[test]
343    #[should_panic(expected = "Plugin returned error")]
344    fn test_assert_plugin_ok_macro_fails_on_err() {
345        let result: std::result::Result<i32, &str> = Err("error");
346        assert_plugin_ok!(result);
347    }
348
349    #[test]
350    #[should_panic(expected = "Custom message")]
351    fn test_assert_plugin_ok_macro_with_message() {
352        let result: std::result::Result<i32, &str> = Err("error");
353        assert_plugin_ok!(result, "Custom message");
354    }
355
356    #[test]
357    fn test_assert_plugin_err_macro() {
358        let result: std::result::Result<i32, &str> = Err("error");
359        assert_plugin_err!(result);
360    }
361
362    #[test]
363    #[should_panic(expected = "Expected plugin error, got success")]
364    fn test_assert_plugin_err_macro_fails_on_ok() {
365        let result: std::result::Result<i32, &str> = Ok(42);
366        assert_plugin_err!(result);
367    }
368
369    #[test]
370    #[should_panic(expected = "Custom message")]
371    fn test_assert_plugin_err_macro_with_message() {
372        let result: std::result::Result<i32, &str> = Ok(42);
373        assert_plugin_err!(result, "Custom message");
374    }
375
376    #[test]
377    fn test_mock_auth_request_basic_auth_special_chars() {
378        let request = MockAuthRequest::with_basic_auth("user@domain.com", "p@ss:w0rd!");
379        let (username, password) = request.basic_credentials().unwrap();
380        assert_eq!(username, "user@domain.com");
381        assert_eq!(password, "p@ss:w0rd!");
382    }
383
384    #[test]
385    fn test_context_version_is_0_1_0() {
386        let mut harness = TestHarness::new();
387        let context = harness.create_context("plugin", "req");
388        assert_eq!(context.version.major, 0);
389        assert_eq!(context.version.minor, 1);
390        assert_eq!(context.version.patch, 0);
391    }
392}