1use crate::error::{Error, Result};
4use reqwest::Client;
5use serde_json::Value;
6use std::time::Duration;
7use tracing::{debug, info};
8
9pub struct ScenarioManager {
11 client: Client,
12 base_url: String,
13}
14
15impl ScenarioManager {
16 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 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 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 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 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 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 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
185pub struct ScenarioBuilder {
187 name: String,
188 mocks: Vec<Value>,
189}
190
191impl ScenarioBuilder {
192 pub fn new<S: Into<String>>(name: S) -> Self {
194 Self {
195 name: name.into(),
196 mocks: Vec::new(),
197 }
198 }
199
200 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 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]
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 #[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}