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    #[test]
223    fn test_scenario_builder() {
224        let scenario = ScenarioBuilder::new("test-scenario")
225            .mock(
226                "/users",
227                serde_json::json!({
228                    "users": [
229                        {"id": 1, "name": "Alice"},
230                        {"id": 2, "name": "Bob"}
231                    ]
232                }),
233            )
234            .mock(
235                "/posts",
236                serde_json::json!({
237                    "posts": []
238                }),
239            )
240            .build();
241
242        assert_eq!(scenario["name"], "test-scenario");
243        assert_eq!(scenario["mocks"].as_array().unwrap().len(), 2);
244    }
245
246    #[test]
247    fn test_scenario_manager_creation() {
248        let manager = ScenarioManager::new("localhost", 3000);
249        assert_eq!(manager.base_url, "http://localhost:3000");
250    }
251}