mockforge_test/
scenario.rs

1//! Scenario and workspace management for tests
2
3use crate::error::{Error, Result};
4use reqwest::Client;
5use serde_json::Value;
6use std::time::Duration;
7use tracing::{debug, info};
8
9/// Scenario manager for switching test scenarios
10pub struct ScenarioManager {
11    client: Client,
12    base_url: String,
13}
14
15impl ScenarioManager {
16    /// Create a new scenario manager
17    ///
18    /// # Arguments
19    ///
20    /// * `host` - Server host (e.g., "localhost")
21    /// * `port` - Server port
22    pub fn new(host: &str, port: u16) -> Self {
23        Self {
24            client: Client::builder()
25                .timeout(Duration::from_secs(10))
26                .build()
27                .expect("Failed to build HTTP client"),
28            base_url: format!("http://{}:{}", host, port),
29        }
30    }
31
32    /// Switch to a different scenario/workspace
33    ///
34    /// # Arguments
35    ///
36    /// * `scenario_name` - Name of the scenario to switch to
37    pub async fn switch_scenario(&self, scenario_name: &str) -> Result<()> {
38        info!("Switching to scenario: {}", scenario_name);
39
40        let url = format!("{}/__mockforge/workspace/switch", self.base_url);
41
42        let response = self
43            .client
44            .post(&url)
45            .json(&serde_json::json!({
46                "workspace": scenario_name
47            }))
48            .send()
49            .await?;
50
51        if !response.status().is_success() {
52            return Err(Error::ScenarioError(format!(
53                "Failed to switch scenario: HTTP {} - {}",
54                response.status(),
55                response.text().await.unwrap_or_default()
56            )));
57        }
58
59        debug!("Successfully switched to scenario: {}", scenario_name);
60        Ok(())
61    }
62
63    /// Load a workspace configuration from a file
64    ///
65    /// # Arguments
66    ///
67    /// * `workspace_file` - Path to the workspace configuration file (JSON or YAML)
68    pub async fn load_workspace<P: AsRef<std::path::Path>>(&self, workspace_file: P) -> Result<()> {
69        let path = workspace_file.as_ref();
70        info!("Loading workspace from: {}", path.display());
71
72        let content = tokio::fs::read_to_string(path)
73            .await
74            .map_err(|e| Error::WorkspaceError(format!("Failed to read workspace file: {}", e)))?;
75
76        let workspace: Value = if path.extension().and_then(|s| s.to_str()) == Some("yaml")
77            || path.extension().and_then(|s| s.to_str()) == Some("yml")
78        {
79            serde_yaml::from_str(&content)?
80        } else {
81            serde_json::from_str(&content)?
82        };
83
84        let url = format!("{}/__mockforge/workspace/load", self.base_url);
85
86        let response = self.client.post(&url).json(&workspace).send().await?;
87
88        if !response.status().is_success() {
89            return Err(Error::WorkspaceError(format!(
90                "Failed to load workspace: HTTP {} - {}",
91                response.status(),
92                response.text().await.unwrap_or_default()
93            )));
94        }
95
96        debug!("Successfully loaded workspace from: {}", path.display());
97        Ok(())
98    }
99
100    /// Update mock configuration dynamically
101    ///
102    /// # Arguments
103    ///
104    /// * `endpoint` - The endpoint path to configure (e.g., "/users")
105    /// * `config` - The mock configuration as JSON
106    pub async fn update_mock(&self, endpoint: &str, config: Value) -> Result<()> {
107        info!("Updating mock for endpoint: {}", endpoint);
108
109        let url = format!("{}/__mockforge/config{}", self.base_url, endpoint);
110
111        let response = self.client.post(&url).json(&config).send().await?;
112
113        if !response.status().is_success() {
114            return Err(Error::ScenarioError(format!(
115                "Failed to update mock: HTTP {} - {}",
116                response.status(),
117                response.text().await.unwrap_or_default()
118            )));
119        }
120
121        debug!("Successfully updated mock for: {}", endpoint);
122        Ok(())
123    }
124
125    /// List available fixtures
126    pub async fn list_fixtures(&self) -> Result<Vec<String>> {
127        debug!("Listing available fixtures");
128
129        let url = format!("{}/__mockforge/fixtures", self.base_url);
130
131        let response = self.client.get(&url).send().await?;
132
133        if !response.status().is_success() {
134            return Err(Error::ScenarioError(format!(
135                "Failed to list fixtures: HTTP {}",
136                response.status()
137            )));
138        }
139
140        let fixtures: Vec<String> = response.json().await?;
141        debug!("Found {} fixtures", fixtures.len());
142
143        Ok(fixtures)
144    }
145
146    /// Get server statistics
147    pub async fn get_stats(&self) -> Result<Value> {
148        debug!("Fetching server statistics");
149
150        let url = format!("{}/__mockforge/stats", self.base_url);
151
152        let response = self.client.get(&url).send().await?;
153
154        if !response.status().is_success() {
155            return Err(Error::InvalidResponse(format!(
156                "Failed to get stats: HTTP {}",
157                response.status()
158            )));
159        }
160
161        let stats: Value = response.json().await?;
162        Ok(stats)
163    }
164
165    /// Reset all mocks to their initial state
166    pub async fn reset(&self) -> Result<()> {
167        info!("Resetting all mocks");
168
169        let url = format!("{}/__mockforge/reset", self.base_url);
170
171        let response = self.client.post(&url).send().await?;
172
173        if !response.status().is_success() {
174            return Err(Error::ScenarioError(format!(
175                "Failed to reset mocks: HTTP {}",
176                response.status()
177            )));
178        }
179
180        debug!("Successfully reset all mocks");
181        Ok(())
182    }
183}
184
185/// Builder for creating scenario configurations
186pub struct ScenarioBuilder {
187    name: String,
188    mocks: Vec<Value>,
189}
190
191impl ScenarioBuilder {
192    /// Create a new scenario builder
193    pub fn new<S: Into<String>>(name: S) -> Self {
194        Self {
195            name: name.into(),
196            mocks: Vec::new(),
197        }
198    }
199
200    /// Add a mock endpoint
201    pub fn mock(mut self, endpoint: &str, response: Value) -> Self {
202        self.mocks.push(serde_json::json!({
203            "endpoint": endpoint,
204            "response": response
205        }));
206        self
207    }
208
209    /// Build the scenario configuration
210    pub fn build(self) -> Value {
211        serde_json::json!({
212            "name": self.name,
213            "mocks": self.mocks
214        })
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    // ScenarioBuilder tests
223    #[test]
224    fn test_scenario_builder() {
225        let scenario = ScenarioBuilder::new("test-scenario")
226            .mock(
227                "/users",
228                serde_json::json!({
229                    "users": [
230                        {"id": 1, "name": "Alice"},
231                        {"id": 2, "name": "Bob"}
232                    ]
233                }),
234            )
235            .mock(
236                "/posts",
237                serde_json::json!({
238                    "posts": []
239                }),
240            )
241            .build();
242
243        assert_eq!(scenario["name"], "test-scenario");
244        assert_eq!(scenario["mocks"].as_array().unwrap().len(), 2);
245    }
246
247    #[test]
248    fn test_scenario_builder_new() {
249        let builder = ScenarioBuilder::new("my-scenario");
250        let scenario = builder.build();
251        assert_eq!(scenario["name"], "my-scenario");
252        assert!(scenario["mocks"].as_array().unwrap().is_empty());
253    }
254
255    #[test]
256    fn test_scenario_builder_with_string_name() {
257        let name = String::from("string-scenario");
258        let scenario = ScenarioBuilder::new(name).build();
259        assert_eq!(scenario["name"], "string-scenario");
260    }
261
262    #[test]
263    fn test_scenario_builder_single_mock() {
264        let scenario = ScenarioBuilder::new("single-mock")
265            .mock("/api/health", serde_json::json!({"status": "ok"}))
266            .build();
267
268        let mocks = scenario["mocks"].as_array().unwrap();
269        assert_eq!(mocks.len(), 1);
270        assert_eq!(mocks[0]["endpoint"], "/api/health");
271    }
272
273    #[test]
274    fn test_scenario_builder_multiple_mocks() {
275        let scenario = ScenarioBuilder::new("multi-mock")
276            .mock("/api/v1/users", serde_json::json!([]))
277            .mock("/api/v1/posts", serde_json::json!([]))
278            .mock("/api/v1/comments", serde_json::json!([]))
279            .build();
280
281        let mocks = scenario["mocks"].as_array().unwrap();
282        assert_eq!(mocks.len(), 3);
283    }
284
285    #[test]
286    fn test_scenario_builder_complex_response() {
287        let response = serde_json::json!({
288            "data": {
289                "user": {
290                    "id": 123,
291                    "name": "John Doe",
292                    "roles": ["admin", "user"],
293                    "metadata": {
294                        "created_at": "2025-01-01T00:00:00Z"
295                    }
296                }
297            },
298            "pagination": {
299                "total": 100,
300                "page": 1,
301                "per_page": 10
302            }
303        });
304
305        let scenario =
306            ScenarioBuilder::new("complex").mock("/api/profile", response.clone()).build();
307
308        let mocks = scenario["mocks"].as_array().unwrap();
309        assert_eq!(mocks[0]["response"]["data"]["user"]["id"], 123);
310    }
311
312    #[test]
313    fn test_scenario_builder_null_response() {
314        let scenario = ScenarioBuilder::new("null-response")
315            .mock("/api/empty", serde_json::json!(null))
316            .build();
317
318        let mocks = scenario["mocks"].as_array().unwrap();
319        assert!(mocks[0]["response"].is_null());
320    }
321
322    #[test]
323    fn test_scenario_builder_array_response() {
324        let scenario = ScenarioBuilder::new("array-response")
325            .mock("/api/items", serde_json::json!([1, 2, 3, 4, 5]))
326            .build();
327
328        let mocks = scenario["mocks"].as_array().unwrap();
329        let response = mocks[0]["response"].as_array().unwrap();
330        assert_eq!(response.len(), 5);
331    }
332
333    // ScenarioManager tests
334    #[test]
335    fn test_scenario_manager_creation() {
336        let manager = ScenarioManager::new("localhost", 3000);
337        assert_eq!(manager.base_url, "http://localhost:3000");
338    }
339
340    #[test]
341    fn test_scenario_manager_different_host() {
342        let manager = ScenarioManager::new("192.168.1.100", 8080);
343        assert_eq!(manager.base_url, "http://192.168.1.100:8080");
344    }
345
346    #[test]
347    fn test_scenario_manager_hostname() {
348        let manager = ScenarioManager::new("api.example.com", 443);
349        assert_eq!(manager.base_url, "http://api.example.com:443");
350    }
351
352    #[test]
353    fn test_scenario_manager_port_zero() {
354        let manager = ScenarioManager::new("localhost", 0);
355        assert_eq!(manager.base_url, "http://localhost:0");
356    }
357
358    #[test]
359    fn test_scenario_manager_high_port() {
360        let manager = ScenarioManager::new("localhost", 65535);
361        assert_eq!(manager.base_url, "http://localhost:65535");
362    }
363}