1use crate::{Error, Result};
7use reqwest::Client;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11pub struct AdminClient {
13 base_url: String,
14 client: Client,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct MockConfig {
20 #[serde(skip_serializing_if = "String::is_empty")]
22 pub id: String,
23 pub name: String,
25 pub method: String,
27 pub path: String,
29 pub response: MockResponse,
31 #[serde(default = "default_true")]
33 pub enabled: bool,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub latency_ms: Option<u64>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub status_code: Option<u16>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub request_match: Option<RequestMatchCriteria>,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub priority: Option<i32>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub scenario: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub required_scenario_state: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub new_scenario_state: Option<String>,
55}
56
57const fn default_true() -> bool {
58 true
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct MockResponse {
64 pub body: serde_json::Value,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub headers: Option<HashMap<String, String>>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, Default)]
73pub struct RequestMatchCriteria {
74 #[serde(skip_serializing_if = "HashMap::is_empty")]
76 pub headers: HashMap<String, String>,
77 #[serde(skip_serializing_if = "HashMap::is_empty")]
79 pub query_params: HashMap<String, String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub body_pattern: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub json_path: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub xpath: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub custom_matcher: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ServerStats {
97 pub uptime_seconds: u64,
99 pub total_requests: u64,
101 pub active_mocks: usize,
103 pub enabled_mocks: usize,
105 pub registered_routes: usize,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ServerConfig {
112 pub version: String,
114 pub port: u16,
116 pub has_openapi_spec: bool,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub spec_path: Option<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct MockList {
126 pub mocks: Vec<MockConfig>,
128 pub total: usize,
130 pub enabled: usize,
132}
133
134impl AdminClient {
135 pub fn new(base_url: impl Into<String>) -> Self {
150 let mut url = base_url.into();
151
152 while url.ends_with('/') {
154 url.pop();
155 }
156
157 Self {
158 base_url: url,
159 client: Client::new(),
160 }
161 }
162
163 pub async fn list_mocks(&self) -> Result<MockList> {
165 let url = format!("{}/api/mocks", self.base_url);
166 let response = self
167 .client
168 .get(&url)
169 .send()
170 .await
171 .map_err(|e| Error::General(format!("Failed to list mocks: {e}")))?;
172
173 if !response.status().is_success() {
174 return Err(Error::General(format!(
175 "Failed to list mocks: HTTP {}",
176 response.status()
177 )));
178 }
179
180 response
181 .json()
182 .await
183 .map_err(|e| Error::General(format!("Failed to parse response: {e}")))
184 }
185
186 pub async fn get_mock(&self, id: &str) -> Result<MockConfig> {
188 let url = format!("{}/api/mocks/{}", self.base_url, id);
189 let response = self
190 .client
191 .get(&url)
192 .send()
193 .await
194 .map_err(|e| Error::General(format!("Failed to get mock: {e}")))?;
195
196 if response.status() == reqwest::StatusCode::NOT_FOUND {
197 return Err(Error::General(format!("Mock not found: {id}")));
198 }
199
200 if !response.status().is_success() {
201 return Err(Error::General(format!("Failed to get mock: HTTP {}", response.status())));
202 }
203
204 response
205 .json()
206 .await
207 .map_err(|e| Error::General(format!("Failed to parse response: {e}")))
208 }
209
210 pub async fn create_mock(&self, mock: MockConfig) -> Result<MockConfig> {
212 let url = format!("{}/api/mocks", self.base_url);
213 let response = self
214 .client
215 .post(&url)
216 .json(&mock)
217 .send()
218 .await
219 .map_err(|e| Error::General(format!("Failed to create mock: {e}")))?;
220
221 if response.status() == reqwest::StatusCode::CONFLICT {
222 return Err(Error::General(format!("Mock with ID {} already exists", mock.id)));
223 }
224
225 if !response.status().is_success() {
226 return Err(Error::General(format!(
227 "Failed to create mock: HTTP {}",
228 response.status()
229 )));
230 }
231
232 response
233 .json()
234 .await
235 .map_err(|e| Error::General(format!("Failed to parse response: {e}")))
236 }
237
238 pub async fn update_mock(&self, id: &str, mock: MockConfig) -> Result<MockConfig> {
240 let url = format!("{}/api/mocks/{}", self.base_url, id);
241 let response = self
242 .client
243 .put(&url)
244 .json(&mock)
245 .send()
246 .await
247 .map_err(|e| Error::General(format!("Failed to update mock: {e}")))?;
248
249 if response.status() == reqwest::StatusCode::NOT_FOUND {
250 return Err(Error::General(format!("Mock not found: {id}")));
251 }
252
253 if !response.status().is_success() {
254 return Err(Error::General(format!(
255 "Failed to update mock: HTTP {}",
256 response.status()
257 )));
258 }
259
260 response
261 .json()
262 .await
263 .map_err(|e| Error::General(format!("Failed to parse response: {e}")))
264 }
265
266 pub async fn delete_mock(&self, id: &str) -> Result<()> {
268 let url = format!("{}/api/mocks/{}", self.base_url, id);
269 let response = self
270 .client
271 .delete(&url)
272 .send()
273 .await
274 .map_err(|e| Error::General(format!("Failed to delete mock: {e}")))?;
275
276 if response.status() == reqwest::StatusCode::NOT_FOUND {
277 return Err(Error::General(format!("Mock not found: {id}")));
278 }
279
280 if !response.status().is_success() {
281 return Err(Error::General(format!(
282 "Failed to delete mock: HTTP {}",
283 response.status()
284 )));
285 }
286
287 Ok(())
288 }
289
290 pub async fn get_stats(&self) -> Result<ServerStats> {
292 let url = format!("{}/api/stats", self.base_url);
293 let response = self
294 .client
295 .get(&url)
296 .send()
297 .await
298 .map_err(|e| Error::General(format!("Failed to get stats: {e}")))?;
299
300 if !response.status().is_success() {
301 return Err(Error::General(format!("Failed to get stats: HTTP {}", response.status())));
302 }
303
304 response
305 .json()
306 .await
307 .map_err(|e| Error::General(format!("Failed to parse response: {e}")))
308 }
309
310 pub async fn get_config(&self) -> Result<ServerConfig> {
312 let url = format!("{}/api/config", self.base_url);
313 let response = self
314 .client
315 .get(&url)
316 .send()
317 .await
318 .map_err(|e| Error::General(format!("Failed to get config: {e}")))?;
319
320 if !response.status().is_success() {
321 return Err(Error::General(format!(
322 "Failed to get config: HTTP {}",
323 response.status()
324 )));
325 }
326
327 response
328 .json()
329 .await
330 .map_err(|e| Error::General(format!("Failed to parse response: {e}")))
331 }
332
333 pub async fn reset(&self) -> Result<()> {
335 let url = format!("{}/api/reset", self.base_url);
336 let response = self
337 .client
338 .post(&url)
339 .send()
340 .await
341 .map_err(|e| Error::General(format!("Failed to reset mocks: {e}")))?;
342
343 if !response.status().is_success() {
344 return Err(Error::General(format!(
345 "Failed to reset mocks: HTTP {}",
346 response.status()
347 )));
348 }
349
350 Ok(())
351 }
352}
353
354pub struct MockConfigBuilder {
384 config: MockConfig,
385}
386
387impl MockConfigBuilder {
388 pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
394 Self {
395 config: MockConfig {
396 id: String::new(),
397 name: String::new(),
398 method: method.into().to_uppercase(),
399 path: path.into(),
400 response: MockResponse {
401 body: serde_json::json!({}),
402 headers: None,
403 },
404 enabled: true,
405 latency_ms: None,
406 status_code: None,
407 request_match: None,
408 priority: None,
409 scenario: None,
410 required_scenario_state: None,
411 new_scenario_state: None,
412 },
413 }
414 }
415
416 pub fn id(mut self, id: impl Into<String>) -> Self {
418 self.config.id = id.into();
419 self
420 }
421
422 pub fn name(mut self, name: impl Into<String>) -> Self {
424 self.config.name = name.into();
425 self
426 }
427
428 #[must_use]
430 pub fn body(mut self, body: serde_json::Value) -> Self {
431 self.config.response.body = body;
432 self
433 }
434
435 #[must_use]
437 pub const fn status(mut self, status: u16) -> Self {
438 self.config.status_code = Some(status);
439 self
440 }
441
442 #[must_use]
444 pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
445 self.config.response.headers = Some(headers);
446 self
447 }
448
449 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
451 let headers = self.config.response.headers.get_or_insert_with(HashMap::new);
452 headers.insert(key.into(), value.into());
453 self
454 }
455
456 #[must_use]
458 pub const fn latency_ms(mut self, ms: u64) -> Self {
459 self.config.latency_ms = Some(ms);
460 self
461 }
462
463 #[must_use]
465 pub const fn enabled(mut self, enabled: bool) -> Self {
466 self.config.enabled = enabled;
467 self
468 }
469
470 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
481 let match_criteria =
482 self.config.request_match.get_or_insert_with(RequestMatchCriteria::default);
483 match_criteria.headers.insert(name.into(), value.into());
484 self
485 }
486
487 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
489 let match_criteria =
490 self.config.request_match.get_or_insert_with(RequestMatchCriteria::default);
491 match_criteria.headers.extend(headers);
492 self
493 }
494
495 pub fn with_query_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
504 let match_criteria =
505 self.config.request_match.get_or_insert_with(RequestMatchCriteria::default);
506 match_criteria.query_params.insert(name.into(), value.into());
507 self
508 }
509
510 pub fn with_query_params(mut self, params: HashMap<String, String>) -> Self {
512 let match_criteria =
513 self.config.request_match.get_or_insert_with(RequestMatchCriteria::default);
514 match_criteria.query_params.extend(params);
515 self
516 }
517
518 pub fn with_body_pattern(mut self, pattern: impl Into<String>) -> Self {
527 let match_criteria =
528 self.config.request_match.get_or_insert_with(RequestMatchCriteria::default);
529 match_criteria.body_pattern = Some(pattern.into());
530 self
531 }
532
533 pub fn with_json_path(mut self, json_path: impl Into<String>) -> Self {
542 let match_criteria =
543 self.config.request_match.get_or_insert_with(RequestMatchCriteria::default);
544 match_criteria.json_path = Some(json_path.into());
545 self
546 }
547
548 pub fn with_xpath(mut self, xpath: impl Into<String>) -> Self {
556 let match_criteria =
557 self.config.request_match.get_or_insert_with(RequestMatchCriteria::default);
558 match_criteria.xpath = Some(xpath.into());
559 self
560 }
561
562 pub fn with_custom_matcher(mut self, expression: impl Into<String>) -> Self {
571 let match_criteria =
572 self.config.request_match.get_or_insert_with(RequestMatchCriteria::default);
573 match_criteria.custom_matcher = Some(expression.into());
574 self
575 }
576
577 #[must_use]
583 pub const fn priority(mut self, priority: i32) -> Self {
584 self.config.priority = Some(priority);
585 self
586 }
587
588 pub fn scenario(mut self, scenario: impl Into<String>) -> Self {
593 self.config.scenario = Some(scenario.into());
594 self
595 }
596
597 pub fn when_scenario_state(mut self, state: impl Into<String>) -> Self {
601 self.config.required_scenario_state = Some(state.into());
602 self
603 }
604
605 pub fn will_set_scenario_state(mut self, state: impl Into<String>) -> Self {
609 self.config.new_scenario_state = Some(state.into());
610 self
611 }
612
613 #[must_use]
615 pub fn build(self) -> MockConfig {
616 self.config
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn test_mock_config_builder_basic() {
626 let mock = MockConfigBuilder::new("GET", "/api/users")
627 .name("Get Users")
628 .status(200)
629 .body(serde_json::json!([{"id": 1, "name": "Alice"}]))
630 .latency_ms(100)
631 .header("Content-Type", "application/json")
632 .build();
633
634 assert_eq!(mock.method, "GET");
635 assert_eq!(mock.path, "/api/users");
636 assert_eq!(mock.name, "Get Users");
637 assert_eq!(mock.status_code, Some(200));
638 assert_eq!(mock.latency_ms, Some(100));
639 assert!(mock.enabled);
640 }
641
642 #[test]
643 fn test_mock_config_builder_with_matching() {
644 let mut headers = HashMap::new();
645 headers.insert("Authorization".to_string(), "Bearer.*".to_string());
646
647 let mut query_params = HashMap::new();
648 query_params.insert("role".to_string(), "admin".to_string());
649
650 let mock = MockConfigBuilder::new("POST", "/api/users")
651 .name("Create User")
652 .with_headers(headers.clone())
653 .with_query_params(query_params.clone())
654 .with_body_pattern(r#"{"name":".*"}"#)
655 .status(201)
656 .body(serde_json::json!({"id": 123, "created": true}))
657 .priority(10)
658 .build();
659
660 assert_eq!(mock.method, "POST");
661 assert!(mock.request_match.is_some());
662 let match_criteria = mock.request_match.unwrap();
663 assert_eq!(match_criteria.headers.get("Authorization"), Some(&"Bearer.*".to_string()));
664 assert_eq!(match_criteria.query_params.get("role"), Some(&"admin".to_string()));
665 assert_eq!(match_criteria.body_pattern, Some(r#"{"name":".*"}"#.to_string()));
666 assert_eq!(mock.priority, Some(10));
667 }
668
669 #[test]
670 fn test_mock_config_builder_with_scenario() {
671 let mock = MockConfigBuilder::new("GET", "/api/checkout")
672 .name("Checkout Step 1")
673 .scenario("checkout-flow")
674 .when_scenario_state("started")
675 .will_set_scenario_state("payment")
676 .status(200)
677 .body(serde_json::json!({"step": 1}))
678 .build();
679
680 assert_eq!(mock.scenario, Some("checkout-flow".to_string()));
681 assert_eq!(mock.required_scenario_state, Some("started".to_string()));
682 assert_eq!(mock.new_scenario_state, Some("payment".to_string()));
683 }
684
685 #[test]
686 fn test_mock_config_builder_fluent_chaining() {
687 let mock = MockConfigBuilder::new("GET", "/api/users/{id}")
688 .id("user-get-123")
689 .name("Get User by ID")
690 .with_header("Accept", "application/json")
691 .with_query_param("include", "profile")
692 .with_json_path("$.id")
693 .status(200)
694 .body(serde_json::json!({"id": "{{request.path.id}}", "name": "Alice"}))
695 .header("X-Request-ID", "{{uuid}}")
696 .latency_ms(50)
697 .priority(5)
698 .enabled(true)
699 .build();
700
701 assert_eq!(mock.id, "user-get-123");
702 assert_eq!(mock.name, "Get User by ID");
703 assert!(mock.request_match.is_some());
704 let match_criteria = mock.request_match.unwrap();
705 assert!(match_criteria.headers.contains_key("Accept"));
706 assert!(match_criteria.query_params.contains_key("include"));
707 assert_eq!(match_criteria.json_path, Some("$.id".to_string()));
708 assert_eq!(mock.priority, Some(5));
709 }
710}