1use mockforge_core::ResourceIdExtract as CoreResourceIdExtract;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10pub type DynamicResponseFn = Arc<dyn Fn(&RequestContext) -> Value + Send + Sync>;
12
13#[derive(Debug, Clone)]
15pub struct RequestContext {
16 pub method: String,
18 pub path: String,
20 pub path_params: HashMap<String, String>,
22 pub query_params: HashMap<String, String>,
24 pub headers: HashMap<String, String>,
26 pub body: Option<Value>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct StateMachineConfig {
33 pub resource_type: String,
35 #[serde(flatten)]
37 pub resource_id_extract: ResourceIdExtractConfig,
38 pub initial_state: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
43 pub state_responses: Option<HashMap<String, StateResponseOverride>>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(tag = "extract_type", rename_all = "snake_case")]
49pub enum ResourceIdExtractConfig {
50 PathParam {
52 param: String,
54 },
55 JsonPath {
57 path: String,
59 },
60 Header {
62 name: String,
64 },
65 QueryParam {
67 param: String,
69 },
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct StateResponseOverride {
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub status: Option<u16>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub body: Option<Value>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub headers: Option<HashMap<String, String>>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, Default)]
88pub struct StubFaultInjectionConfig {
89 #[serde(default)]
91 pub enabled: bool,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub http_errors: Option<Vec<u16>>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub http_error_probability: Option<f64>,
98 #[serde(default)]
100 pub timeout_error: bool,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub timeout_ms: Option<u64>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub timeout_probability: Option<f64>,
107 #[serde(default)]
109 pub connection_error: bool,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub connection_error_probability: Option<f64>,
113}
114
115impl StubFaultInjectionConfig {
116 #[must_use]
118 pub fn http_error(codes: Vec<u16>) -> Self {
119 Self {
120 enabled: true,
121 http_errors: Some(codes),
122 http_error_probability: Some(1.0),
123 ..Default::default()
124 }
125 }
126
127 #[must_use]
129 pub fn timeout(ms: u64) -> Self {
130 Self {
131 enabled: true,
132 timeout_error: true,
133 timeout_ms: Some(ms),
134 timeout_probability: Some(1.0),
135 ..Default::default()
136 }
137 }
138
139 #[must_use]
141 pub fn connection_error() -> Self {
142 Self {
143 enabled: true,
144 connection_error: true,
145 connection_error_probability: Some(1.0),
146 ..Default::default()
147 }
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ResponseStub {
154 pub method: String,
156 pub path: String,
158 pub status: u16,
160 pub headers: HashMap<String, String>,
162 pub body: Value,
164 pub latency_ms: Option<u64>,
166 #[serde(skip_serializing_if = "Option::is_none")]
169 pub state_machine: Option<StateMachineConfig>,
170 #[serde(skip_serializing_if = "Option::is_none")]
173 pub fault_injection: Option<StubFaultInjectionConfig>,
174}
175
176impl ResponseStub {
177 pub fn new(method: impl Into<String>, path: impl Into<String>, body: Value) -> Self {
179 Self {
180 method: method.into(),
181 path: path.into(),
182 status: 200,
183 headers: HashMap::new(),
184 body,
185 latency_ms: None,
186 state_machine: None,
187 fault_injection: None,
188 }
189 }
190
191 #[must_use]
193 pub const fn status(mut self, status: u16) -> Self {
194 self.status = status;
195 self
196 }
197
198 #[must_use]
200 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
201 self.headers.insert(key.into(), value.into());
202 self
203 }
204
205 #[must_use]
207 pub const fn latency(mut self, ms: u64) -> Self {
208 self.latency_ms = Some(ms);
209 self
210 }
211
212 #[must_use]
214 pub fn with_state_machine(mut self, config: StateMachineConfig) -> Self {
215 self.state_machine = Some(config);
216 self
217 }
218
219 #[must_use]
221 pub const fn has_state_machine(&self) -> bool {
222 self.state_machine.is_some()
223 }
224
225 #[must_use]
227 pub const fn state_machine(&self) -> Option<&StateMachineConfig> {
228 self.state_machine.as_ref()
229 }
230
231 #[must_use]
239 pub fn apply_state_override(&self, current_state: &str) -> Self {
240 let mut stub = self.clone();
241
242 if let Some(ref state_machine) = self.state_machine {
243 if let Some(ref state_responses) = state_machine.state_responses {
244 if let Some(override_config) = state_responses.get(current_state) {
245 if let Some(status) = override_config.status {
247 stub.status = status;
248 }
249
250 if let Some(ref body) = override_config.body {
252 stub.body = body.clone();
253 }
254
255 if let Some(ref headers) = override_config.headers {
257 for (key, value) in headers {
258 stub.headers.insert(key.clone(), value.clone());
259 }
260 }
261 }
262 }
263 }
264
265 stub
266 }
267
268 #[must_use]
270 pub fn with_fault_injection(mut self, config: StubFaultInjectionConfig) -> Self {
271 self.fault_injection = Some(config);
272 self
273 }
274
275 #[must_use]
277 pub fn has_fault_injection(&self) -> bool {
278 self.fault_injection.as_ref().is_some_and(|f| f.enabled)
279 }
280
281 #[must_use]
283 pub const fn fault_injection(&self) -> Option<&StubFaultInjectionConfig> {
284 self.fault_injection.as_ref()
285 }
286}
287
288impl ResourceIdExtractConfig {
289 #[must_use]
291 pub fn to_core(&self) -> CoreResourceIdExtract {
292 match self {
293 Self::PathParam { param } => CoreResourceIdExtract::PathParam {
294 param: param.clone(),
295 },
296 Self::JsonPath { path } => CoreResourceIdExtract::JsonPath { path: path.clone() },
297 Self::Header { name } => CoreResourceIdExtract::Header { name: name.clone() },
298 Self::QueryParam { param } => CoreResourceIdExtract::QueryParam {
299 param: param.clone(),
300 },
301 }
302 }
303}
304
305pub struct DynamicStub {
307 pub method: String,
309 pub path: String,
311 pub status: Arc<RwLock<u16>>,
313 pub headers: Arc<RwLock<HashMap<String, String>>>,
315 pub response_fn: DynamicResponseFn,
317 pub latency_ms: Option<u64>,
319}
320
321impl DynamicStub {
322 pub fn new<F>(method: impl Into<String>, path: impl Into<String>, response_fn: F) -> Self
324 where
325 F: Fn(&RequestContext) -> Value + Send + Sync + 'static,
326 {
327 Self {
328 method: method.into(),
329 path: path.into(),
330 status: Arc::new(RwLock::new(200)),
331 headers: Arc::new(RwLock::new(HashMap::new())),
332 response_fn: Arc::new(response_fn),
333 latency_ms: None,
334 }
335 }
336
337 pub async fn set_status(&self, status: u16) {
339 *self.status.write().await = status;
340 }
341
342 pub async fn get_status(&self) -> u16 {
344 *self.status.read().await
345 }
346
347 pub async fn add_header(&self, key: String, value: String) {
349 self.headers.write().await.insert(key, value);
350 }
351
352 pub async fn remove_header(&self, key: &str) {
354 self.headers.write().await.remove(key);
355 }
356
357 pub async fn get_headers(&self) -> HashMap<String, String> {
361 self.headers.read().await.clone()
362 }
363
364 pub async fn with_headers<F, R>(&self, f: F) -> R
385 where
386 F: FnOnce(&HashMap<String, String>) -> R,
387 {
388 let headers = self.headers.read().await;
389 f(&headers)
390 }
391
392 #[must_use]
394 pub fn generate_response(&self, ctx: &RequestContext) -> Value {
395 (self.response_fn)(ctx)
396 }
397
398 #[must_use]
400 pub const fn with_latency(mut self, ms: u64) -> Self {
401 self.latency_ms = Some(ms);
402 self
403 }
404}
405
406pub struct StubBuilder {
424 method: String,
425 path: String,
426 status: u16,
427 headers: HashMap<String, String>,
428 body: Option<Value>,
429 latency_ms: Option<u64>,
430 state_machine: Option<StateMachineConfig>,
431 fault_injection: Option<StubFaultInjectionConfig>,
432}
433
434impl StubBuilder {
435 pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
441 Self {
442 method: method.into(),
443 path: path.into(),
444 status: 200,
445 headers: HashMap::new(),
446 body: None,
447 latency_ms: None,
448 state_machine: None,
449 fault_injection: None,
450 }
451 }
452
453 #[must_use]
455 pub const fn status(mut self, status: u16) -> Self {
456 self.status = status;
457 self
458 }
459
460 #[must_use]
462 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
463 self.headers.insert(key.into(), value.into());
464 self
465 }
466
467 #[must_use]
469 pub fn body(mut self, body: Value) -> Self {
470 self.body = Some(body);
471 self
472 }
473
474 #[must_use]
476 pub const fn latency(mut self, ms: u64) -> Self {
477 self.latency_ms = Some(ms);
478 self
479 }
480
481 #[must_use]
483 pub fn state_machine(mut self, config: StateMachineConfig) -> Self {
484 self.state_machine = Some(config);
485 self
486 }
487
488 #[must_use]
490 pub fn fault_injection(mut self, config: StubFaultInjectionConfig) -> Self {
491 self.fault_injection = Some(config);
492 self
493 }
494
495 #[must_use]
497 pub fn build(self) -> ResponseStub {
498 ResponseStub {
499 method: self.method,
500 path: self.path,
501 status: self.status,
502 headers: self.headers,
503 body: self.body.unwrap_or(Value::Null),
504 latency_ms: self.latency_ms,
505 state_machine: self.state_machine,
506 fault_injection: self.fault_injection,
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use serde_json::json;
515
516 #[test]
519 fn test_request_context_creation() {
520 let ctx = RequestContext {
521 method: "GET".to_string(),
522 path: "/api/users".to_string(),
523 path_params: HashMap::from([("id".to_string(), "123".to_string())]),
524 query_params: HashMap::from([("page".to_string(), "1".to_string())]),
525 headers: HashMap::from([("content-type".to_string(), "application/json".to_string())]),
526 body: Some(json!({"name": "test"})),
527 };
528
529 assert_eq!(ctx.method, "GET");
530 assert_eq!(ctx.path, "/api/users");
531 assert_eq!(ctx.path_params.get("id"), Some(&"123".to_string()));
532 }
533
534 #[test]
535 fn test_request_context_clone() {
536 let ctx = RequestContext {
537 method: "POST".to_string(),
538 path: "/api/users".to_string(),
539 path_params: HashMap::new(),
540 query_params: HashMap::new(),
541 headers: HashMap::new(),
542 body: None,
543 };
544
545 let cloned = ctx.clone();
546 assert_eq!(ctx.method, cloned.method);
547 assert_eq!(ctx.path, cloned.path);
548 }
549
550 #[test]
553 fn test_state_machine_config_serialize() {
554 let config = StateMachineConfig {
555 resource_type: "order".to_string(),
556 resource_id_extract: ResourceIdExtractConfig::PathParam {
557 param: "order_id".to_string(),
558 },
559 initial_state: "pending".to_string(),
560 state_responses: None,
561 };
562
563 let json = serde_json::to_string(&config).unwrap();
564 assert!(json.contains("order"));
565 assert!(json.contains("pending"));
566 }
567
568 #[test]
569 fn test_state_machine_config_with_responses() {
570 let mut responses = HashMap::new();
571 responses.insert(
572 "confirmed".to_string(),
573 StateResponseOverride {
574 status: Some(200),
575 body: Some(json!({"status": "confirmed"})),
576 headers: None,
577 },
578 );
579
580 let config = StateMachineConfig {
581 resource_type: "order".to_string(),
582 resource_id_extract: ResourceIdExtractConfig::PathParam {
583 param: "order_id".to_string(),
584 },
585 initial_state: "pending".to_string(),
586 state_responses: Some(responses),
587 };
588
589 assert!(config.state_responses.is_some());
590 }
591
592 #[test]
595 fn test_resource_id_extract_path_param() {
596 let config = ResourceIdExtractConfig::PathParam {
597 param: "user_id".to_string(),
598 };
599 let core = config.to_core();
600 match core {
601 CoreResourceIdExtract::PathParam { param } => assert_eq!(param, "user_id"),
602 _ => panic!("Expected PathParam"),
603 }
604 }
605
606 #[test]
607 fn test_resource_id_extract_json_path() {
608 let config = ResourceIdExtractConfig::JsonPath {
609 path: "$.data.id".to_string(),
610 };
611 let core = config.to_core();
612 match core {
613 CoreResourceIdExtract::JsonPath { path } => assert_eq!(path, "$.data.id"),
614 _ => panic!("Expected JsonPath"),
615 }
616 }
617
618 #[test]
619 fn test_resource_id_extract_header() {
620 let config = ResourceIdExtractConfig::Header {
621 name: "X-Resource-Id".to_string(),
622 };
623 let core = config.to_core();
624 match core {
625 CoreResourceIdExtract::Header { name } => assert_eq!(name, "X-Resource-Id"),
626 _ => panic!("Expected Header"),
627 }
628 }
629
630 #[test]
631 fn test_resource_id_extract_query_param() {
632 let config = ResourceIdExtractConfig::QueryParam {
633 param: "id".to_string(),
634 };
635 let core = config.to_core();
636 match core {
637 CoreResourceIdExtract::QueryParam { param } => assert_eq!(param, "id"),
638 _ => panic!("Expected QueryParam"),
639 }
640 }
641
642 #[test]
645 fn test_state_response_override_status_only() {
646 let override_config = StateResponseOverride {
647 status: Some(404),
648 body: None,
649 headers: None,
650 };
651 assert_eq!(override_config.status, Some(404));
652 }
653
654 #[test]
655 fn test_state_response_override_full() {
656 let mut headers = HashMap::new();
657 headers.insert("X-Custom".to_string(), "value".to_string());
658
659 let override_config = StateResponseOverride {
660 status: Some(200),
661 body: Some(json!({"data": "test"})),
662 headers: Some(headers),
663 };
664
665 assert_eq!(override_config.status, Some(200));
666 assert!(override_config.body.is_some());
667 assert!(override_config.headers.is_some());
668 }
669
670 #[test]
673 fn test_stub_fault_injection_default() {
674 let config = StubFaultInjectionConfig::default();
675 assert!(!config.enabled);
676 assert!(config.http_errors.is_none());
677 assert!(!config.timeout_error);
678 assert!(!config.connection_error);
679 }
680
681 #[test]
682 fn test_stub_fault_injection_http_error() {
683 let config = StubFaultInjectionConfig::http_error(vec![500, 502, 503]);
684 assert!(config.enabled);
685 assert_eq!(config.http_errors, Some(vec![500, 502, 503]));
686 assert_eq!(config.http_error_probability, Some(1.0));
687 }
688
689 #[test]
690 fn test_stub_fault_injection_timeout() {
691 let config = StubFaultInjectionConfig::timeout(5000);
692 assert!(config.enabled);
693 assert!(config.timeout_error);
694 assert_eq!(config.timeout_ms, Some(5000));
695 assert_eq!(config.timeout_probability, Some(1.0));
696 }
697
698 #[test]
699 fn test_stub_fault_injection_connection_error() {
700 let config = StubFaultInjectionConfig::connection_error();
701 assert!(config.enabled);
702 assert!(config.connection_error);
703 assert_eq!(config.connection_error_probability, Some(1.0));
704 }
705
706 #[test]
709 fn test_response_stub_new() {
710 let stub = ResponseStub::new("GET", "/api/users", json!({"users": []}));
711 assert_eq!(stub.method, "GET");
712 assert_eq!(stub.path, "/api/users");
713 assert_eq!(stub.status, 200);
714 assert!(stub.headers.is_empty());
715 assert!(stub.latency_ms.is_none());
716 }
717
718 #[test]
719 fn test_response_stub_status() {
720 let stub = ResponseStub::new("GET", "/api/users", json!({})).status(404);
721 assert_eq!(stub.status, 404);
722 }
723
724 #[test]
725 fn test_response_stub_header() {
726 let stub = ResponseStub::new("GET", "/api/users", json!({}))
727 .header("Content-Type", "application/json")
728 .header("X-Custom", "value");
729
730 assert_eq!(stub.headers.get("Content-Type"), Some(&"application/json".to_string()));
731 assert_eq!(stub.headers.get("X-Custom"), Some(&"value".to_string()));
732 }
733
734 #[test]
735 fn test_response_stub_latency() {
736 let stub = ResponseStub::new("GET", "/api/users", json!({})).latency(100);
737 assert_eq!(stub.latency_ms, Some(100));
738 }
739
740 #[test]
741 fn test_response_stub_with_state_machine() {
742 let state_config = StateMachineConfig {
743 resource_type: "user".to_string(),
744 resource_id_extract: ResourceIdExtractConfig::PathParam {
745 param: "user_id".to_string(),
746 },
747 initial_state: "active".to_string(),
748 state_responses: None,
749 };
750
751 let stub = ResponseStub::new("GET", "/api/users/{user_id}", json!({}))
752 .with_state_machine(state_config);
753
754 assert!(stub.has_state_machine());
755 assert!(stub.state_machine().is_some());
756 }
757
758 #[test]
759 fn test_response_stub_no_state_machine() {
760 let stub = ResponseStub::new("GET", "/api/users", json!({}));
761 assert!(!stub.has_state_machine());
762 assert!(stub.state_machine().is_none());
763 }
764
765 #[test]
766 fn test_response_stub_apply_state_override() {
767 let mut state_responses = HashMap::new();
768 state_responses.insert(
769 "inactive".to_string(),
770 StateResponseOverride {
771 status: Some(403),
772 body: Some(json!({"error": "User is inactive"})),
773 headers: Some(HashMap::from([("X-State".to_string(), "inactive".to_string())])),
774 },
775 );
776
777 let state_config = StateMachineConfig {
778 resource_type: "user".to_string(),
779 resource_id_extract: ResourceIdExtractConfig::PathParam {
780 param: "user_id".to_string(),
781 },
782 initial_state: "active".to_string(),
783 state_responses: Some(state_responses),
784 };
785
786 let stub = ResponseStub::new("GET", "/api/users/{user_id}", json!({"status": "ok"}))
787 .with_state_machine(state_config);
788
789 let overridden = stub.apply_state_override("inactive");
791 assert_eq!(overridden.status, 403);
792 assert_eq!(overridden.body, json!({"error": "User is inactive"}));
793 assert_eq!(overridden.headers.get("X-State"), Some(&"inactive".to_string()));
794 }
795
796 #[test]
797 fn test_response_stub_apply_state_override_no_match() {
798 let state_config = StateMachineConfig {
799 resource_type: "user".to_string(),
800 resource_id_extract: ResourceIdExtractConfig::PathParam {
801 param: "user_id".to_string(),
802 },
803 initial_state: "active".to_string(),
804 state_responses: None,
805 };
806
807 let stub = ResponseStub::new("GET", "/api/users/{user_id}", json!({"original": true}))
808 .status(200)
809 .with_state_machine(state_config);
810
811 let overridden = stub.apply_state_override("unknown");
813 assert_eq!(overridden.status, 200);
814 assert_eq!(overridden.body, json!({"original": true}));
815 }
816
817 #[test]
818 fn test_response_stub_with_fault_injection() {
819 let fault_config = StubFaultInjectionConfig::http_error(vec![500]);
820 let stub =
821 ResponseStub::new("GET", "/api/users", json!({})).with_fault_injection(fault_config);
822
823 assert!(stub.has_fault_injection());
824 assert!(stub.fault_injection().is_some());
825 }
826
827 #[test]
828 fn test_response_stub_no_fault_injection() {
829 let stub = ResponseStub::new("GET", "/api/users", json!({}));
830 assert!(!stub.has_fault_injection());
831 }
832
833 #[test]
834 fn test_response_stub_serialize() {
835 let stub = ResponseStub::new("POST", "/api/orders", json!({"id": 1}))
836 .status(201)
837 .header("Location", "/api/orders/1")
838 .latency(50);
839
840 let json = serde_json::to_string(&stub).unwrap();
841 assert!(json.contains("POST"));
842 assert!(json.contains("/api/orders"));
843 assert!(json.contains("201"));
844 }
845
846 #[test]
849 fn test_dynamic_stub_new() {
850 let stub = DynamicStub::new("GET", "/api/users", |ctx| json!({"path": ctx.path.clone()}));
851
852 assert_eq!(stub.method, "GET");
853 assert_eq!(stub.path, "/api/users");
854 }
855
856 #[tokio::test]
857 async fn test_dynamic_stub_status() {
858 let stub = DynamicStub::new("GET", "/test", |_| json!({}));
859 assert_eq!(stub.get_status().await, 200);
860
861 stub.set_status(404).await;
862 assert_eq!(stub.get_status().await, 404);
863 }
864
865 #[tokio::test]
866 async fn test_dynamic_stub_headers() {
867 let stub = DynamicStub::new("GET", "/test", |_| json!({}));
868
869 stub.add_header("X-Custom".to_string(), "value".to_string()).await;
870
871 let headers = stub.get_headers().await;
872 assert_eq!(headers.get("X-Custom"), Some(&"value".to_string()));
873
874 stub.remove_header("X-Custom").await;
875 let headers = stub.get_headers().await;
876 assert!(!headers.contains_key("X-Custom"));
877 }
878
879 #[tokio::test]
880 async fn test_dynamic_stub_with_headers() {
881 let stub = DynamicStub::new("GET", "/test", |_| json!({}));
882 stub.add_header("X-Test".to_string(), "test-value".to_string()).await;
883
884 let has_header = stub.with_headers(|headers| headers.contains_key("X-Test")).await;
885 assert!(has_header);
886 }
887
888 #[test]
889 fn test_dynamic_stub_generate_response() {
890 let stub = DynamicStub::new("GET", "/api/users/{id}", |ctx| {
891 let id = ctx.path_params.get("id").cloned().unwrap_or_default();
892 json!({"user_id": id})
893 });
894
895 let ctx = RequestContext {
896 method: "GET".to_string(),
897 path: "/api/users/123".to_string(),
898 path_params: HashMap::from([("id".to_string(), "123".to_string())]),
899 query_params: HashMap::new(),
900 headers: HashMap::new(),
901 body: None,
902 };
903
904 let response = stub.generate_response(&ctx);
905 assert_eq!(response, json!({"user_id": "123"}));
906 }
907
908 #[test]
909 fn test_dynamic_stub_with_latency() {
910 let stub = DynamicStub::new("GET", "/test", |_| json!({})).with_latency(100);
911 assert_eq!(stub.latency_ms, Some(100));
912 }
913
914 #[test]
917 fn test_stub_builder_basic() {
918 let stub = StubBuilder::new("GET", "/api/users").body(json!({"users": []})).build();
919
920 assert_eq!(stub.method, "GET");
921 assert_eq!(stub.path, "/api/users");
922 assert_eq!(stub.status, 200);
923 }
924
925 #[test]
926 fn test_stub_builder_status() {
927 let stub = StubBuilder::new("GET", "/api/users").status(404).build();
928
929 assert_eq!(stub.status, 404);
930 }
931
932 #[test]
933 fn test_stub_builder_headers() {
934 let stub = StubBuilder::new("GET", "/api/users")
935 .header("Content-Type", "application/json")
936 .header("X-Custom", "value")
937 .build();
938
939 assert_eq!(stub.headers.len(), 2);
940 }
941
942 #[test]
943 fn test_stub_builder_latency() {
944 let stub = StubBuilder::new("GET", "/api/users").latency(500).build();
945
946 assert_eq!(stub.latency_ms, Some(500));
947 }
948
949 #[test]
950 fn test_stub_builder_state_machine() {
951 let config = StateMachineConfig {
952 resource_type: "order".to_string(),
953 resource_id_extract: ResourceIdExtractConfig::PathParam {
954 param: "order_id".to_string(),
955 },
956 initial_state: "pending".to_string(),
957 state_responses: None,
958 };
959
960 let stub = StubBuilder::new("GET", "/api/orders/{order_id}").state_machine(config).build();
961
962 assert!(stub.state_machine.is_some());
963 }
964
965 #[test]
966 fn test_stub_builder_fault_injection() {
967 let fault = StubFaultInjectionConfig::http_error(vec![500]);
968
969 let stub = StubBuilder::new("GET", "/api/users").fault_injection(fault).build();
970
971 assert!(stub.fault_injection.is_some());
972 }
973
974 #[test]
975 fn test_stub_builder_full_chain() {
976 let stub = StubBuilder::new("POST", "/api/orders")
977 .status(201)
978 .header("Location", "/api/orders/1")
979 .body(json!({"id": 1, "status": "created"}))
980 .latency(100)
981 .build();
982
983 assert_eq!(stub.method, "POST");
984 assert_eq!(stub.path, "/api/orders");
985 assert_eq!(stub.status, 201);
986 assert_eq!(stub.headers.get("Location"), Some(&"/api/orders/1".to_string()));
987 assert_eq!(stub.latency_ms, Some(100));
988 }
989}