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 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
200 self.headers.insert(key.into(), value.into());
201 self
202 }
203
204 #[must_use]
206 pub const fn latency(mut self, ms: u64) -> Self {
207 self.latency_ms = Some(ms);
208 self
209 }
210
211 #[must_use]
213 pub fn with_state_machine(mut self, config: StateMachineConfig) -> Self {
214 self.state_machine = Some(config);
215 self
216 }
217
218 #[must_use]
220 pub const fn has_state_machine(&self) -> bool {
221 self.state_machine.is_some()
222 }
223
224 #[must_use]
226 pub const fn state_machine(&self) -> Option<&StateMachineConfig> {
227 self.state_machine.as_ref()
228 }
229
230 #[must_use]
238 pub fn apply_state_override(&self, current_state: &str) -> Self {
239 let mut stub = self.clone();
240
241 if let Some(ref state_machine) = self.state_machine {
242 if let Some(ref state_responses) = state_machine.state_responses {
243 if let Some(override_config) = state_responses.get(current_state) {
244 if let Some(status) = override_config.status {
246 stub.status = status;
247 }
248
249 if let Some(ref body) = override_config.body {
251 stub.body = body.clone();
252 }
253
254 if let Some(ref headers) = override_config.headers {
256 for (key, value) in headers {
257 stub.headers.insert(key.clone(), value.clone());
258 }
259 }
260 }
261 }
262 }
263
264 stub
265 }
266
267 #[must_use]
269 pub fn with_fault_injection(mut self, config: StubFaultInjectionConfig) -> Self {
270 self.fault_injection = Some(config);
271 self
272 }
273
274 #[must_use]
276 pub fn has_fault_injection(&self) -> bool {
277 self.fault_injection.as_ref().is_some_and(|f| f.enabled)
278 }
279
280 #[must_use]
282 pub const fn fault_injection(&self) -> Option<&StubFaultInjectionConfig> {
283 self.fault_injection.as_ref()
284 }
285}
286
287impl ResourceIdExtractConfig {
288 #[must_use]
290 pub fn to_core(&self) -> CoreResourceIdExtract {
291 match self {
292 Self::PathParam { param } => CoreResourceIdExtract::PathParam {
293 param: param.clone(),
294 },
295 Self::JsonPath { path } => CoreResourceIdExtract::JsonPath { path: path.clone() },
296 Self::Header { name } => CoreResourceIdExtract::Header { name: name.clone() },
297 Self::QueryParam { param } => CoreResourceIdExtract::QueryParam {
298 param: param.clone(),
299 },
300 }
301 }
302}
303
304pub struct DynamicStub {
306 pub method: String,
308 pub path: String,
310 pub status: Arc<RwLock<u16>>,
312 pub headers: Arc<RwLock<HashMap<String, String>>>,
314 pub response_fn: DynamicResponseFn,
316 pub latency_ms: Option<u64>,
318}
319
320impl DynamicStub {
321 pub fn new<F>(method: impl Into<String>, path: impl Into<String>, response_fn: F) -> Self
323 where
324 F: Fn(&RequestContext) -> Value + Send + Sync + 'static,
325 {
326 Self {
327 method: method.into(),
328 path: path.into(),
329 status: Arc::new(RwLock::new(200)),
330 headers: Arc::new(RwLock::new(HashMap::new())),
331 response_fn: Arc::new(response_fn),
332 latency_ms: None,
333 }
334 }
335
336 pub async fn set_status(&self, status: u16) {
338 *self.status.write().await = status;
339 }
340
341 pub async fn get_status(&self) -> u16 {
343 *self.status.read().await
344 }
345
346 pub async fn add_header(&self, key: String, value: String) {
348 self.headers.write().await.insert(key, value);
349 }
350
351 pub async fn remove_header(&self, key: &str) {
353 self.headers.write().await.remove(key);
354 }
355
356 pub async fn get_headers(&self) -> HashMap<String, String> {
360 self.headers.read().await.clone()
361 }
362
363 pub async fn with_headers<F, R>(&self, f: F) -> R
384 where
385 F: FnOnce(&HashMap<String, String>) -> R,
386 {
387 let headers = self.headers.read().await;
388 f(&headers)
389 }
390
391 #[must_use]
393 pub fn generate_response(&self, ctx: &RequestContext) -> Value {
394 (self.response_fn)(ctx)
395 }
396
397 #[must_use]
399 pub const fn with_latency(mut self, ms: u64) -> Self {
400 self.latency_ms = Some(ms);
401 self
402 }
403}
404
405pub struct StubBuilder {
423 method: String,
424 path: String,
425 status: u16,
426 headers: HashMap<String, String>,
427 body: Option<Value>,
428 latency_ms: Option<u64>,
429 state_machine: Option<StateMachineConfig>,
430 fault_injection: Option<StubFaultInjectionConfig>,
431}
432
433impl StubBuilder {
434 pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
440 Self {
441 method: method.into(),
442 path: path.into(),
443 status: 200,
444 headers: HashMap::new(),
445 body: None,
446 latency_ms: None,
447 state_machine: None,
448 fault_injection: None,
449 }
450 }
451
452 #[must_use]
454 pub const fn status(mut self, status: u16) -> Self {
455 self.status = status;
456 self
457 }
458
459 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
461 self.headers.insert(key.into(), value.into());
462 self
463 }
464
465 #[must_use]
467 pub fn body(mut self, body: Value) -> Self {
468 self.body = Some(body);
469 self
470 }
471
472 #[must_use]
474 pub const fn latency(mut self, ms: u64) -> Self {
475 self.latency_ms = Some(ms);
476 self
477 }
478
479 #[must_use]
481 pub fn state_machine(mut self, config: StateMachineConfig) -> Self {
482 self.state_machine = Some(config);
483 self
484 }
485
486 #[must_use]
488 pub fn fault_injection(mut self, config: StubFaultInjectionConfig) -> Self {
489 self.fault_injection = Some(config);
490 self
491 }
492
493 #[must_use]
495 pub fn build(self) -> ResponseStub {
496 ResponseStub {
497 method: self.method,
498 path: self.path,
499 status: self.status,
500 headers: self.headers,
501 body: self.body.unwrap_or(Value::Null),
502 latency_ms: self.latency_ms,
503 state_machine: self.state_machine,
504 fault_injection: self.fault_injection,
505 }
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use serde_json::json;
513
514 #[test]
517 fn test_request_context_creation() {
518 let ctx = RequestContext {
519 method: "GET".to_string(),
520 path: "/api/users".to_string(),
521 path_params: HashMap::from([("id".to_string(), "123".to_string())]),
522 query_params: HashMap::from([("page".to_string(), "1".to_string())]),
523 headers: HashMap::from([("content-type".to_string(), "application/json".to_string())]),
524 body: Some(json!({"name": "test"})),
525 };
526
527 assert_eq!(ctx.method, "GET");
528 assert_eq!(ctx.path, "/api/users");
529 assert_eq!(ctx.path_params.get("id"), Some(&"123".to_string()));
530 }
531
532 #[test]
533 fn test_request_context_clone() {
534 let ctx = RequestContext {
535 method: "POST".to_string(),
536 path: "/api/users".to_string(),
537 path_params: HashMap::new(),
538 query_params: HashMap::new(),
539 headers: HashMap::new(),
540 body: None,
541 };
542
543 let cloned = ctx.clone();
544 assert_eq!(ctx.method, cloned.method);
545 assert_eq!(ctx.path, cloned.path);
546 }
547
548 #[test]
551 fn test_state_machine_config_serialize() {
552 let config = StateMachineConfig {
553 resource_type: "order".to_string(),
554 resource_id_extract: ResourceIdExtractConfig::PathParam {
555 param: "order_id".to_string(),
556 },
557 initial_state: "pending".to_string(),
558 state_responses: None,
559 };
560
561 let json = serde_json::to_string(&config).unwrap();
562 assert!(json.contains("order"));
563 assert!(json.contains("pending"));
564 }
565
566 #[test]
567 fn test_state_machine_config_with_responses() {
568 let mut responses = HashMap::new();
569 responses.insert(
570 "confirmed".to_string(),
571 StateResponseOverride {
572 status: Some(200),
573 body: Some(json!({"status": "confirmed"})),
574 headers: None,
575 },
576 );
577
578 let config = StateMachineConfig {
579 resource_type: "order".to_string(),
580 resource_id_extract: ResourceIdExtractConfig::PathParam {
581 param: "order_id".to_string(),
582 },
583 initial_state: "pending".to_string(),
584 state_responses: Some(responses),
585 };
586
587 assert!(config.state_responses.is_some());
588 }
589
590 #[test]
593 fn test_resource_id_extract_path_param() {
594 let config = ResourceIdExtractConfig::PathParam {
595 param: "user_id".to_string(),
596 };
597 let core = config.to_core();
598 match core {
599 CoreResourceIdExtract::PathParam { param } => assert_eq!(param, "user_id"),
600 _ => panic!("Expected PathParam"),
601 }
602 }
603
604 #[test]
605 fn test_resource_id_extract_json_path() {
606 let config = ResourceIdExtractConfig::JsonPath {
607 path: "$.data.id".to_string(),
608 };
609 let core = config.to_core();
610 match core {
611 CoreResourceIdExtract::JsonPath { path } => assert_eq!(path, "$.data.id"),
612 _ => panic!("Expected JsonPath"),
613 }
614 }
615
616 #[test]
617 fn test_resource_id_extract_header() {
618 let config = ResourceIdExtractConfig::Header {
619 name: "X-Resource-Id".to_string(),
620 };
621 let core = config.to_core();
622 match core {
623 CoreResourceIdExtract::Header { name } => assert_eq!(name, "X-Resource-Id"),
624 _ => panic!("Expected Header"),
625 }
626 }
627
628 #[test]
629 fn test_resource_id_extract_query_param() {
630 let config = ResourceIdExtractConfig::QueryParam {
631 param: "id".to_string(),
632 };
633 let core = config.to_core();
634 match core {
635 CoreResourceIdExtract::QueryParam { param } => assert_eq!(param, "id"),
636 _ => panic!("Expected QueryParam"),
637 }
638 }
639
640 #[test]
643 fn test_state_response_override_status_only() {
644 let override_config = StateResponseOverride {
645 status: Some(404),
646 body: None,
647 headers: None,
648 };
649 assert_eq!(override_config.status, Some(404));
650 }
651
652 #[test]
653 fn test_state_response_override_full() {
654 let mut headers = HashMap::new();
655 headers.insert("X-Custom".to_string(), "value".to_string());
656
657 let override_config = StateResponseOverride {
658 status: Some(200),
659 body: Some(json!({"data": "test"})),
660 headers: Some(headers),
661 };
662
663 assert_eq!(override_config.status, Some(200));
664 assert!(override_config.body.is_some());
665 assert!(override_config.headers.is_some());
666 }
667
668 #[test]
671 fn test_stub_fault_injection_default() {
672 let config = StubFaultInjectionConfig::default();
673 assert!(!config.enabled);
674 assert!(config.http_errors.is_none());
675 assert!(!config.timeout_error);
676 assert!(!config.connection_error);
677 }
678
679 #[test]
680 fn test_stub_fault_injection_http_error() {
681 let config = StubFaultInjectionConfig::http_error(vec![500, 502, 503]);
682 assert!(config.enabled);
683 assert_eq!(config.http_errors, Some(vec![500, 502, 503]));
684 assert_eq!(config.http_error_probability, Some(1.0));
685 }
686
687 #[test]
688 fn test_stub_fault_injection_timeout() {
689 let config = StubFaultInjectionConfig::timeout(5000);
690 assert!(config.enabled);
691 assert!(config.timeout_error);
692 assert_eq!(config.timeout_ms, Some(5000));
693 assert_eq!(config.timeout_probability, Some(1.0));
694 }
695
696 #[test]
697 fn test_stub_fault_injection_connection_error() {
698 let config = StubFaultInjectionConfig::connection_error();
699 assert!(config.enabled);
700 assert!(config.connection_error);
701 assert_eq!(config.connection_error_probability, Some(1.0));
702 }
703
704 #[test]
707 fn test_response_stub_new() {
708 let stub = ResponseStub::new("GET", "/api/users", json!({"users": []}));
709 assert_eq!(stub.method, "GET");
710 assert_eq!(stub.path, "/api/users");
711 assert_eq!(stub.status, 200);
712 assert!(stub.headers.is_empty());
713 assert!(stub.latency_ms.is_none());
714 }
715
716 #[test]
717 fn test_response_stub_status() {
718 let stub = ResponseStub::new("GET", "/api/users", json!({})).status(404);
719 assert_eq!(stub.status, 404);
720 }
721
722 #[test]
723 fn test_response_stub_header() {
724 let stub = ResponseStub::new("GET", "/api/users", json!({}))
725 .header("Content-Type", "application/json")
726 .header("X-Custom", "value");
727
728 assert_eq!(stub.headers.get("Content-Type"), Some(&"application/json".to_string()));
729 assert_eq!(stub.headers.get("X-Custom"), Some(&"value".to_string()));
730 }
731
732 #[test]
733 fn test_response_stub_latency() {
734 let stub = ResponseStub::new("GET", "/api/users", json!({})).latency(100);
735 assert_eq!(stub.latency_ms, Some(100));
736 }
737
738 #[test]
739 fn test_response_stub_with_state_machine() {
740 let state_config = StateMachineConfig {
741 resource_type: "user".to_string(),
742 resource_id_extract: ResourceIdExtractConfig::PathParam {
743 param: "user_id".to_string(),
744 },
745 initial_state: "active".to_string(),
746 state_responses: None,
747 };
748
749 let stub = ResponseStub::new("GET", "/api/users/{user_id}", json!({}))
750 .with_state_machine(state_config);
751
752 assert!(stub.has_state_machine());
753 assert!(stub.state_machine().is_some());
754 }
755
756 #[test]
757 fn test_response_stub_no_state_machine() {
758 let stub = ResponseStub::new("GET", "/api/users", json!({}));
759 assert!(!stub.has_state_machine());
760 assert!(stub.state_machine().is_none());
761 }
762
763 #[test]
764 fn test_response_stub_apply_state_override() {
765 let mut state_responses = HashMap::new();
766 state_responses.insert(
767 "inactive".to_string(),
768 StateResponseOverride {
769 status: Some(403),
770 body: Some(json!({"error": "User is inactive"})),
771 headers: Some(HashMap::from([("X-State".to_string(), "inactive".to_string())])),
772 },
773 );
774
775 let state_config = StateMachineConfig {
776 resource_type: "user".to_string(),
777 resource_id_extract: ResourceIdExtractConfig::PathParam {
778 param: "user_id".to_string(),
779 },
780 initial_state: "active".to_string(),
781 state_responses: Some(state_responses),
782 };
783
784 let stub = ResponseStub::new("GET", "/api/users/{user_id}", json!({"status": "ok"}))
785 .with_state_machine(state_config);
786
787 let overridden = stub.apply_state_override("inactive");
789 assert_eq!(overridden.status, 403);
790 assert_eq!(overridden.body, json!({"error": "User is inactive"}));
791 assert_eq!(overridden.headers.get("X-State"), Some(&"inactive".to_string()));
792 }
793
794 #[test]
795 fn test_response_stub_apply_state_override_no_match() {
796 let state_config = StateMachineConfig {
797 resource_type: "user".to_string(),
798 resource_id_extract: ResourceIdExtractConfig::PathParam {
799 param: "user_id".to_string(),
800 },
801 initial_state: "active".to_string(),
802 state_responses: None,
803 };
804
805 let stub = ResponseStub::new("GET", "/api/users/{user_id}", json!({"original": true}))
806 .status(200)
807 .with_state_machine(state_config);
808
809 let overridden = stub.apply_state_override("unknown");
811 assert_eq!(overridden.status, 200);
812 assert_eq!(overridden.body, json!({"original": true}));
813 }
814
815 #[test]
816 fn test_response_stub_with_fault_injection() {
817 let fault_config = StubFaultInjectionConfig::http_error(vec![500]);
818 let stub =
819 ResponseStub::new("GET", "/api/users", json!({})).with_fault_injection(fault_config);
820
821 assert!(stub.has_fault_injection());
822 assert!(stub.fault_injection().is_some());
823 }
824
825 #[test]
826 fn test_response_stub_no_fault_injection() {
827 let stub = ResponseStub::new("GET", "/api/users", json!({}));
828 assert!(!stub.has_fault_injection());
829 }
830
831 #[test]
832 fn test_response_stub_serialize() {
833 let stub = ResponseStub::new("POST", "/api/orders", json!({"id": 1}))
834 .status(201)
835 .header("Location", "/api/orders/1")
836 .latency(50);
837
838 let json = serde_json::to_string(&stub).unwrap();
839 assert!(json.contains("POST"));
840 assert!(json.contains("/api/orders"));
841 assert!(json.contains("201"));
842 }
843
844 #[test]
847 fn test_dynamic_stub_new() {
848 let stub = DynamicStub::new("GET", "/api/users", |ctx| json!({"path": ctx.path.clone()}));
849
850 assert_eq!(stub.method, "GET");
851 assert_eq!(stub.path, "/api/users");
852 }
853
854 #[tokio::test]
855 async fn test_dynamic_stub_status() {
856 let stub = DynamicStub::new("GET", "/test", |_| json!({}));
857 assert_eq!(stub.get_status().await, 200);
858
859 stub.set_status(404).await;
860 assert_eq!(stub.get_status().await, 404);
861 }
862
863 #[tokio::test]
864 async fn test_dynamic_stub_headers() {
865 let stub = DynamicStub::new("GET", "/test", |_| json!({}));
866
867 stub.add_header("X-Custom".to_string(), "value".to_string()).await;
868
869 let headers = stub.get_headers().await;
870 assert_eq!(headers.get("X-Custom"), Some(&"value".to_string()));
871
872 stub.remove_header("X-Custom").await;
873 let headers = stub.get_headers().await;
874 assert!(headers.get("X-Custom").is_none());
875 }
876
877 #[tokio::test]
878 async fn test_dynamic_stub_with_headers() {
879 let stub = DynamicStub::new("GET", "/test", |_| json!({}));
880 stub.add_header("X-Test".to_string(), "test-value".to_string()).await;
881
882 let has_header = stub.with_headers(|headers| headers.contains_key("X-Test")).await;
883 assert!(has_header);
884 }
885
886 #[test]
887 fn test_dynamic_stub_generate_response() {
888 let stub = DynamicStub::new("GET", "/api/users/{id}", |ctx| {
889 let id = ctx.path_params.get("id").cloned().unwrap_or_default();
890 json!({"user_id": id})
891 });
892
893 let ctx = RequestContext {
894 method: "GET".to_string(),
895 path: "/api/users/123".to_string(),
896 path_params: HashMap::from([("id".to_string(), "123".to_string())]),
897 query_params: HashMap::new(),
898 headers: HashMap::new(),
899 body: None,
900 };
901
902 let response = stub.generate_response(&ctx);
903 assert_eq!(response, json!({"user_id": "123"}));
904 }
905
906 #[test]
907 fn test_dynamic_stub_with_latency() {
908 let stub = DynamicStub::new("GET", "/test", |_| json!({})).with_latency(100);
909 assert_eq!(stub.latency_ms, Some(100));
910 }
911
912 #[test]
915 fn test_stub_builder_basic() {
916 let stub = StubBuilder::new("GET", "/api/users").body(json!({"users": []})).build();
917
918 assert_eq!(stub.method, "GET");
919 assert_eq!(stub.path, "/api/users");
920 assert_eq!(stub.status, 200);
921 }
922
923 #[test]
924 fn test_stub_builder_status() {
925 let stub = StubBuilder::new("GET", "/api/users").status(404).build();
926
927 assert_eq!(stub.status, 404);
928 }
929
930 #[test]
931 fn test_stub_builder_headers() {
932 let stub = StubBuilder::new("GET", "/api/users")
933 .header("Content-Type", "application/json")
934 .header("X-Custom", "value")
935 .build();
936
937 assert_eq!(stub.headers.len(), 2);
938 }
939
940 #[test]
941 fn test_stub_builder_latency() {
942 let stub = StubBuilder::new("GET", "/api/users").latency(500).build();
943
944 assert_eq!(stub.latency_ms, Some(500));
945 }
946
947 #[test]
948 fn test_stub_builder_state_machine() {
949 let config = StateMachineConfig {
950 resource_type: "order".to_string(),
951 resource_id_extract: ResourceIdExtractConfig::PathParam {
952 param: "order_id".to_string(),
953 },
954 initial_state: "pending".to_string(),
955 state_responses: None,
956 };
957
958 let stub = StubBuilder::new("GET", "/api/orders/{order_id}").state_machine(config).build();
959
960 assert!(stub.state_machine.is_some());
961 }
962
963 #[test]
964 fn test_stub_builder_fault_injection() {
965 let fault = StubFaultInjectionConfig::http_error(vec![500]);
966
967 let stub = StubBuilder::new("GET", "/api/users").fault_injection(fault).build();
968
969 assert!(stub.fault_injection.is_some());
970 }
971
972 #[test]
973 fn test_stub_builder_full_chain() {
974 let stub = StubBuilder::new("POST", "/api/orders")
975 .status(201)
976 .header("Location", "/api/orders/1")
977 .body(json!({"id": 1, "status": "created"}))
978 .latency(100)
979 .build();
980
981 assert_eq!(stub.method, "POST");
982 assert_eq!(stub.path, "/api/orders");
983 assert_eq!(stub.status, 201);
984 assert_eq!(stub.headers.get("Location"), Some(&"/api/orders/1".to_string()));
985 assert_eq!(stub.latency_ms, Some(100));
986 }
987}