1use crate::{MockResponse, Rule, RuleMode};
7use aws_smithy_http_client::test_util::infallible_client_fn;
8use aws_smithy_runtime_api::box_error::BoxError;
9use aws_smithy_runtime_api::client::http::SharedHttpClient;
10use aws_smithy_runtime_api::client::interceptors::context::{
11 BeforeSerializationInterceptorContextRef, BeforeTransmitInterceptorContextMut, Error,
12 FinalizerInterceptorContextMut, Input, Output,
13};
14use aws_smithy_runtime_api::client::interceptors::Intercept;
15use aws_smithy_runtime_api::client::orchestrator::{HttpResponse, OrchestratorError};
16use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
17use aws_smithy_types::body::SdkBody;
18use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace};
19use std::collections::{HashMap, VecDeque};
20use std::fmt;
21use std::sync::atomic::{AtomicUsize, Ordering};
22use std::sync::{Arc, Mutex};
23
24#[derive(Debug, Clone)]
26struct ActiveRule(Rule);
27
28impl Storable for ActiveRule {
29 type Storer = StoreReplace<ActiveRule>;
30}
31
32#[derive(Debug, Clone)]
34struct ResponseId(usize);
35
36impl Storable for ResponseId {
37 type Storer = StoreReplace<ResponseId>;
38}
39
40pub struct MockResponseInterceptor {
42 rules: Arc<Mutex<VecDeque<Rule>>>,
43 rule_mode: RuleMode,
44 must_match: bool,
45 active_responses: Arc<Mutex<HashMap<usize, MockResponse<Output, Error>>>>,
46 current_response_id: Arc<AtomicUsize>,
50}
51
52impl fmt::Debug for MockResponseInterceptor {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 write!(f, "{} rules", self.rules.lock().unwrap().len())
55 }
56}
57
58impl Default for MockResponseInterceptor {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl MockResponseInterceptor {
65 pub fn new() -> Self {
69 Self {
70 rules: Default::default(),
71 rule_mode: RuleMode::MatchAny,
72 must_match: true,
73 active_responses: Default::default(),
74 current_response_id: Default::default(),
75 }
76 }
77 pub fn with_rule(self, rule: &Rule) -> Self {
81 self.rules.lock().unwrap().push_back(rule.clone());
82 self
83 }
84
85 pub fn rule_mode(mut self, rule_mode: RuleMode) -> Self {
89 self.rule_mode = rule_mode;
90 self
91 }
92
93 pub fn allow_passthrough(mut self) -> Self {
98 self.must_match = false;
99 self
100 }
101}
102
103impl Intercept for MockResponseInterceptor {
104 fn name(&self) -> &'static str {
105 "MockResponseInterceptor"
106 }
107
108 fn read_before_serialization(
109 &self,
110 context: &BeforeSerializationInterceptorContextRef<'_>,
111 _runtime_components: &RuntimeComponents,
112 cfg: &mut ConfigBag,
113 ) -> Result<(), BoxError> {
114 let mut rules = self.rules.lock().unwrap();
115 let input = context.inner().input().expect("input set");
116
117 let mut matching_rule = None;
119 let mut matching_response = None;
120
121 match self.rule_mode {
122 RuleMode::Sequential => {
123 let i = 0;
125 while i < rules.len() && matching_response.is_none() {
126 let rule = &rules[i];
127
128 if rule.is_exhausted() || (rule.is_simple() && rule.num_calls() > 0) {
134 rules.remove(i);
136 continue; }
138
139 if !(rule.matcher)(input) {
141 panic!("In order matching was enforced but rule did not match {input:?}");
143 }
144
145 if let Some(response) = rule.next_response(input) {
147 matching_rule = Some(rule.clone());
148 matching_response = Some(response);
149 } else {
150 rules.remove(i);
152 continue; }
154
155 break;
157 }
158 }
159 RuleMode::MatchAny => {
160 for rule in rules.iter() {
162 if rule.is_exhausted() {
164 continue;
165 }
166
167 if (rule.matcher)(input) {
168 if let Some(response) = rule.next_response(input) {
169 matching_rule = Some(rule.clone());
170 matching_response = Some(response);
171 break;
172 }
173 }
174 }
175 }
176 };
177
178 match (matching_rule, matching_response) {
179 (Some(rule), Some(response)) => {
180 cfg.interceptor_state().store_put(ActiveRule(rule));
182
183 let response_id = self.current_response_id.fetch_add(1, Ordering::SeqCst);
190 cfg.interceptor_state().store_put(ResponseId(response_id));
191 let mut active_responses = self.active_responses.lock().unwrap();
192 active_responses.insert(response_id, response);
193 }
194 _ => {
195 if self.must_match {
197 panic!(
198 "must_match was enabled but no rules matched or all rules were exhausted for {input:?}"
199 );
200 }
201 }
202 }
203
204 Ok(())
205 }
206
207 fn modify_before_transmit(
208 &self,
209 context: &mut BeforeTransmitInterceptorContextMut<'_>,
210 _runtime_components: &RuntimeComponents,
211 cfg: &mut ConfigBag,
212 ) -> Result<(), BoxError> {
213 if let Some(response_id) = cfg.load::<ResponseId>() {
214 let mut state = self.active_responses.lock().unwrap();
215 let mut active_response = state.remove(&response_id.0);
216 if active_response.is_none() {
217 if let Some(active_rule) = cfg.load::<ActiveRule>() {
219 let dummy_input = Input::doesnt_matter();
222 let next_resp = active_rule.0.next_response(&dummy_input);
223 active_response = next_resp;
224 }
225 }
226
227 if let Some(resp) = active_response {
228 match resp {
229 MockResponse::Http(http_resp) => {
231 context
232 .request_mut()
233 .add_extension(MockHttpResponse(Arc::new(http_resp)));
234 }
235 _ => {
236 state.insert(response_id.0, resp);
238 }
239 }
240 }
241 }
242
243 Ok(())
244 }
245
246 fn modify_before_attempt_completion(
247 &self,
248 context: &mut FinalizerInterceptorContextMut<'_>,
249 _runtime_components: &RuntimeComponents,
250 cfg: &mut ConfigBag,
251 ) -> Result<(), BoxError> {
252 if let Some(response_id) = cfg.load::<ResponseId>() {
254 let mut state = self.active_responses.lock().unwrap();
255 let active_response = state.remove(&response_id.0);
256 if let Some(resp) = active_response {
257 match resp {
258 MockResponse::Output(output) => {
259 context.inner_mut().set_output_or_error(Ok(output));
260 }
261 MockResponse::Error(error) => {
262 context
263 .inner_mut()
264 .set_output_or_error(Err(OrchestratorError::operation(error)));
265 }
266 MockResponse::Http(_) => {
267 }
269 }
270 }
271 }
272
273 Ok(())
274 }
275}
276
277#[derive(Clone)]
279struct MockHttpResponse(Arc<HttpResponse>);
280
281pub fn create_mock_http_client() -> SharedHttpClient {
283 infallible_client_fn(|mut req| {
284 if let Some(mock_response) = req.extensions_mut().remove::<MockHttpResponse>() {
286 let http_resp =
287 Arc::try_unwrap(mock_response.0).expect("mock HTTP response has single reference");
288 return http_resp.try_into_http1x().unwrap();
289 }
290
291 http::Response::builder()
293 .status(418)
294 .body(SdkBody::from("Mock HTTP client dummy response"))
295 .unwrap()
296 })
297}
298
299#[cfg(test)]
300mod tests {
301 use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep};
302 use aws_smithy_runtime::client::orchestrator::operation::Operation;
303 use aws_smithy_runtime::client::retries::classifiers::HttpStatusCodeClassifier;
304 use aws_smithy_runtime_api::client::orchestrator::{
305 HttpRequest, HttpResponse, OrchestratorError,
306 };
307 use aws_smithy_runtime_api::client::result::SdkError;
308 use aws_smithy_runtime_api::http::StatusCode;
309 use aws_smithy_types::body::SdkBody;
310 use aws_smithy_types::retry::RetryConfig;
311 use aws_smithy_types::timeout::TimeoutConfig;
312
313 use crate::{create_mock_http_client, MockResponseInterceptor, RuleBuilder, RuleMode};
314 use std::time::Duration;
315
316 #[derive(Debug)]
318 struct TestInput {
319 bucket: String,
320 key: String,
321 }
322 impl TestInput {
323 fn new(bucket: &str, key: &str) -> Self {
324 Self {
325 bucket: bucket.to_string(),
326 key: key.to_string(),
327 }
328 }
329 }
330
331 #[derive(Debug, PartialEq)]
332 struct TestOutput {
333 content: String,
334 }
335
336 impl TestOutput {
337 fn new(content: &str) -> Self {
338 Self {
339 content: content.to_string(),
340 }
341 }
342 }
343
344 #[derive(Debug)]
345 struct TestError {
346 message: String,
347 }
348
349 impl TestError {
350 fn new(message: &str) -> Self {
351 Self {
352 message: message.to_string(),
353 }
354 }
355 }
356
357 impl std::fmt::Display for TestError {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 write!(f, "{}", self.message)
360 }
361 }
362
363 impl std::error::Error for TestError {}
364
365 fn create_rule_builder() -> RuleBuilder<TestInput, TestOutput, TestError> {
367 RuleBuilder::new_from_mock(
368 || TestInput {
369 bucket: "".to_string(),
370 key: "".to_string(),
371 },
372 || {
373 let fut: std::future::Ready<Result<TestOutput, SdkError<TestError, HttpResponse>>> =
374 std::future::ready(Ok(TestOutput {
375 content: "".to_string(),
376 }));
377 fut
378 },
379 )
380 }
381
382 fn create_test_operation(
384 interceptor: MockResponseInterceptor,
385 enable_retries: bool,
386 ) -> Operation<TestInput, TestOutput, TestError> {
387 let builder = Operation::builder()
388 .service_name("test")
389 .operation_name("test")
390 .http_client(create_mock_http_client())
391 .endpoint_url("http://localhost:1234")
392 .no_auth()
393 .sleep_impl(SharedAsyncSleep::new(TokioSleep::new()))
394 .timeout_config(TimeoutConfig::disabled())
395 .interceptor(interceptor)
396 .serializer(|input: TestInput| {
397 let mut request = HttpRequest::new(SdkBody::empty());
398 request
399 .set_uri(format!("/{}/{}", input.bucket, input.key))
400 .expect("valid URI");
401 Ok(request)
402 })
403 .deserializer::<TestOutput, TestError>(|response| {
404 if response.status().is_success() {
405 let body = std::str::from_utf8(response.body().bytes().unwrap())
406 .unwrap_or("empty body")
407 .to_string();
408 Ok(TestOutput { content: body })
409 } else {
410 Err(OrchestratorError::operation(TestError {
411 message: format!("Error: {}", response.status()),
412 }))
413 }
414 });
415
416 if enable_retries {
417 let retry_config = RetryConfig::standard()
418 .with_max_attempts(5)
419 .with_initial_backoff(Duration::from_millis(1))
420 .with_max_backoff(Duration::from_millis(5));
421
422 builder
423 .retry_classifier(HttpStatusCodeClassifier::default())
424 .standard_retry(&retry_config)
425 .build()
426 } else {
427 builder.no_retry().build()
428 }
429 }
430
431 #[tokio::test]
432 async fn test_retry_sequence() {
433 let rule = create_rule_builder()
435 .match_requests(|input| input.bucket == "test-bucket" && input.key == "test-key")
436 .sequence()
437 .http_status(503, None)
438 .times(2)
439 .output(|| TestOutput::new("success after retries"))
440 .build();
441
442 let interceptor = MockResponseInterceptor::new()
444 .rule_mode(RuleMode::Sequential)
445 .with_rule(&rule);
446
447 let operation = create_test_operation(interceptor, true);
448
449 let result = operation
451 .invoke(TestInput::new("test-bucket", "test-key"))
452 .await;
453
454 assert!(
456 result.is_ok(),
457 "Expected success but got error: {:?}",
458 result.err()
459 );
460 assert_eq!(
461 result.unwrap(),
462 TestOutput {
463 content: "success after retries".to_string()
464 }
465 );
466
467 assert_eq!(rule.num_calls(), 3);
469 }
470
471 #[tokio::test]
472 async fn test_compute_output() {
473 let rule = create_rule_builder()
475 .match_requests(|input| input.bucket == "test-bucket" && input.key == "test-key")
476 .then_compute_output(|input| TestOutput {
477 content: format!("{}.{}", input.bucket, input.key),
478 });
479
480 let interceptor = MockResponseInterceptor::new()
482 .rule_mode(RuleMode::Sequential)
483 .with_rule(&rule);
484
485 let operation = create_test_operation(interceptor, true);
486
487 let result = operation
488 .invoke(TestInput::new("test-bucket", "test-key"))
489 .await;
490
491 assert!(
493 result.is_ok(),
494 "Expected success but got error: {:?}",
495 result.err()
496 );
497 assert_eq!(
498 result.unwrap(),
499 TestOutput {
500 content: "test-bucket.test-key".to_string()
501 }
502 );
503
504 assert_eq!(rule.num_calls(), 1);
506 }
507
508 #[should_panic(
509 expected = "must_match was enabled but no rules matched or all rules were exhausted for"
510 )]
511 #[tokio::test]
512 async fn test_exhausted_rules_sequential() {
513 let rule = create_rule_builder().then_output(|| TestOutput::new("only response"));
515
516 let interceptor = MockResponseInterceptor::new()
518 .rule_mode(RuleMode::Sequential)
519 .with_rule(&rule);
520
521 let operation = create_test_operation(interceptor, false);
522
523 let result1 = operation
525 .invoke(TestInput::new("test-bucket", "test-key"))
526 .await;
527 assert!(result1.is_ok());
528
529 let _result2 = operation
531 .invoke(TestInput::new("test-bucket", "test-key"))
532 .await;
533 }
534
535 #[tokio::test]
536 async fn test_rule_mode_match_any() {
537 let rule1 = create_rule_builder()
539 .match_requests(|input| input.bucket == "bucket1")
540 .then_output(|| TestOutput::new("response1"));
541
542 let rule2 = create_rule_builder()
543 .match_requests(|input| input.bucket == "bucket2")
544 .then_output(|| TestOutput::new("response2"));
545
546 let interceptor = MockResponseInterceptor::new()
548 .rule_mode(RuleMode::MatchAny)
549 .with_rule(&rule1)
550 .with_rule(&rule2);
551
552 let operation = create_test_operation(interceptor, false);
553
554 let result1 = operation
556 .invoke(TestInput::new("bucket1", "test-key"))
557 .await;
558 assert!(result1.is_ok());
559 assert_eq!(result1.unwrap(), TestOutput::new("response1"));
560
561 let result2 = operation
563 .invoke(TestInput::new("bucket2", "test-key"))
564 .await;
565 assert!(result2.is_ok());
566 assert_eq!(result2.unwrap(), TestOutput::new("response2"));
567
568 assert_eq!(rule1.num_calls(), 1);
570 assert_eq!(rule2.num_calls(), 1);
571
572 let result1 = operation
574 .invoke(TestInput::new("bucket1", "test-key"))
575 .await;
576 assert!(result1.is_ok());
577 assert_eq!(result1.unwrap(), TestOutput::new("response1"));
578 assert_eq!(rule1.num_calls(), 2);
579 }
580
581 #[tokio::test]
582 async fn test_mixed_response_types() {
583 let rule = create_rule_builder()
585 .sequence()
586 .output(|| TestOutput::new("first output"))
587 .error(|| TestError::new("expected error"))
588 .http_response(|| {
589 HttpResponse::new(
590 StatusCode::try_from(200).unwrap(),
591 SdkBody::from("http response"),
592 )
593 })
594 .build();
595
596 let interceptor = MockResponseInterceptor::new()
598 .rule_mode(RuleMode::Sequential)
599 .with_rule(&rule);
600
601 let operation = create_test_operation(interceptor, false);
602
603 let result1 = operation
605 .invoke(TestInput::new("test-bucket", "test-key"))
606 .await;
607 assert!(result1.is_ok());
608 assert_eq!(result1.unwrap(), TestOutput::new("first output"));
609
610 let result2 = operation
612 .invoke(TestInput::new("test-bucket", "test-key"))
613 .await;
614 assert!(result2.is_err());
615 let sdk_err = result2.unwrap_err();
616 let err = sdk_err.as_service_error().expect("expected service error");
617 assert_eq!(err.to_string(), "expected error");
618
619 let result3 = operation
621 .invoke(TestInput::new("test-bucket", "test-key"))
622 .await;
623 assert!(result3.is_ok());
624 assert_eq!(result3.unwrap(), TestOutput::new("http response"));
625
626 assert_eq!(rule.num_calls(), 3);
628 }
629 #[tokio::test]
630 async fn test_exhausted_sequence_match_any() {
631 let rule = create_rule_builder()
633 .match_requests(|input| input.bucket == "bucket-1")
634 .sequence()
635 .output(|| TestOutput::new("response 1"))
636 .output(|| TestOutput::new("response 2"))
637 .build();
638
639 let fallback_rule =
641 create_rule_builder().then_output(|| TestOutput::new("fallback response"));
642
643 let interceptor = MockResponseInterceptor::new()
645 .rule_mode(RuleMode::MatchAny)
646 .with_rule(&rule)
647 .with_rule(&fallback_rule);
648
649 let operation = create_test_operation(interceptor, false);
650
651 let result1 = operation
653 .invoke(TestInput::new("bucket-1", "test-key"))
654 .await;
655 assert!(result1.is_ok());
656 assert_eq!(result1.unwrap(), TestOutput::new("response 1"));
657
658 let result2 = operation
660 .invoke(TestInput::new("other-bucket", "test-key"))
661 .await;
662 assert!(result2.is_ok());
663 assert_eq!(result2.unwrap(), TestOutput::new("fallback response"));
664
665 let result3 = operation
667 .invoke(TestInput::new("bucket-1", "test-key"))
668 .await;
669 assert!(result3.is_ok());
670 assert_eq!(result3.unwrap(), TestOutput::new("response 2"));
671
672 let result4 = operation
674 .invoke(TestInput::new("bucket-1", "test-key"))
675 .await;
676 assert!(result4.is_ok());
677 assert_eq!(result4.unwrap(), TestOutput::new("fallback response"));
678
679 assert_eq!(rule.num_calls(), 2);
681 assert_eq!(fallback_rule.num_calls(), 2);
682 }
683
684 #[tokio::test]
685 async fn test_exhausted_sequence_sequential() {
686 let rule = create_rule_builder()
688 .sequence()
689 .output(|| TestOutput::new("response 1"))
690 .output(|| TestOutput::new("response 2"))
691 .build();
692
693 let fallback_rule =
695 create_rule_builder().then_output(|| TestOutput::new("fallback response"));
696
697 let interceptor = MockResponseInterceptor::new()
699 .rule_mode(RuleMode::Sequential)
700 .with_rule(&rule)
701 .with_rule(&fallback_rule);
702
703 let operation = create_test_operation(interceptor, false);
704
705 let result1 = operation
707 .invoke(TestInput::new("test-bucket", "test-key"))
708 .await;
709 assert!(result1.is_ok());
710 assert_eq!(result1.unwrap(), TestOutput::new("response 1"));
711
712 let result2 = operation
713 .invoke(TestInput::new("test-bucket", "test-key"))
714 .await;
715 assert!(result2.is_ok());
716 assert_eq!(result2.unwrap(), TestOutput::new("response 2"));
717
718 let result3 = operation
720 .invoke(TestInput::new("test-bucket", "test-key"))
721 .await;
722 assert!(result3.is_ok());
723 assert_eq!(result3.unwrap(), TestOutput::new("fallback response"));
724
725 assert_eq!(rule.num_calls(), 2);
727 assert_eq!(fallback_rule.num_calls(), 1);
728 }
729
730 #[tokio::test]
731 async fn test_concurrent_usage() {
732 use std::sync::Arc;
733 use tokio::task;
734
735 let rule = Arc::new(
737 create_rule_builder()
738 .sequence()
739 .output(|| TestOutput::new("response 1"))
740 .output(|| TestOutput::new("response 2"))
741 .output(|| TestOutput::new("response 3"))
742 .build(),
743 );
744
745 let interceptor = MockResponseInterceptor::new()
747 .rule_mode(RuleMode::Sequential)
748 .with_rule(&rule);
749
750 let operation = Arc::new(create_test_operation(interceptor, false));
751
752 let mut handles = vec![];
754 for i in 0..3 {
755 let op = operation.clone();
756 let handle = task::spawn(async move {
757 let result = op
758 .invoke(TestInput::new(&format!("bucket-{i}"), "test-key"))
759 .await;
760 result.unwrap()
761 });
762 handles.push(handle);
763 }
764
765 let mut results = vec![];
767 for handle in handles {
768 results.push(handle.await.unwrap());
769 }
770
771 results.sort_by(|a, b| a.content.cmp(&b.content));
773
774 assert_eq!(results.len(), 3);
776 assert_eq!(results[0], TestOutput::new("response 1"));
777 assert_eq!(results[1], TestOutput::new("response 2"));
778 assert_eq!(results[2], TestOutput::new("response 3"));
779
780 assert_eq!(rule.num_calls(), 3);
782 }
783
784 #[tokio::test]
785 async fn test_sequential_rule_removal() {
786 let rule1 = create_rule_builder()
788 .match_requests(|input| input.bucket == "test-bucket" && input.key != "correct-key")
789 .then_http_response(|| {
790 HttpResponse::new(
791 StatusCode::try_from(404).unwrap(),
792 SdkBody::from("not found"),
793 )
794 });
795
796 let rule2 = create_rule_builder()
798 .match_requests(|input| input.bucket == "test-bucket" && input.key == "correct-key")
799 .then_output(|| TestOutput::new("success"));
800
801 let interceptor = MockResponseInterceptor::new()
803 .rule_mode(RuleMode::Sequential)
804 .with_rule(&rule1)
805 .with_rule(&rule2);
806
807 let operation = create_test_operation(interceptor, true);
808
809 let result1 = operation.invoke(TestInput::new("test-bucket", "foo")).await;
811 assert!(result1.is_err());
812 assert_eq!(rule1.num_calls(), 1);
813
814 let result2 = operation
817 .invoke(TestInput::new("test-bucket", "correct-key"))
818 .await;
819
820 assert!(result2.is_ok());
822 assert_eq!(result2.unwrap(), TestOutput::new("success"));
823 assert_eq!(rule2.num_calls(), 1);
824 }
825
826 #[tokio::test]
827 async fn test_simple_rule_in_match_any_mode() {
828 let rule = create_rule_builder().then_output(|| TestOutput::new("simple response"));
829
830 let interceptor = MockResponseInterceptor::new()
831 .rule_mode(RuleMode::MatchAny)
832 .with_rule(&rule);
833
834 let operation = create_test_operation(interceptor, false);
835
836 for i in 0..5 {
837 let result = operation
838 .invoke(TestInput::new("test-bucket", "test-key"))
839 .await;
840 assert!(result.is_ok(), "Call {i} should succeed");
841 assert_eq!(result.unwrap(), TestOutput::new("simple response"));
842 }
843 assert_eq!(rule.num_calls(), 5);
844 assert!(!rule.is_exhausted());
845 }
846
847 #[tokio::test]
848 async fn test_simple_rule_in_sequential_mode() {
849 let rule1 = create_rule_builder().then_output(|| TestOutput::new("first response"));
850 let rule2 = create_rule_builder().then_output(|| TestOutput::new("second response"));
851
852 let interceptor = MockResponseInterceptor::new()
853 .rule_mode(RuleMode::Sequential)
854 .with_rule(&rule1)
855 .with_rule(&rule2);
856
857 let operation = create_test_operation(interceptor, false);
858
859 let result1 = operation
860 .invoke(TestInput::new("test-bucket", "test-key"))
861 .await;
862 assert!(result1.is_ok());
863 assert_eq!(result1.unwrap(), TestOutput::new("first response"));
864
865 let result2 = operation
867 .invoke(TestInput::new("test-bucket", "test-key"))
868 .await;
869 assert!(result2.is_ok());
870 assert_eq!(result2.unwrap(), TestOutput::new("second response"));
871
872 assert_eq!(rule1.num_calls(), 1);
873 assert_eq!(rule2.num_calls(), 1);
874 }
875
876 #[tokio::test]
877 async fn test_repeatedly_method() {
878 let rule = create_rule_builder()
879 .sequence()
880 .output(|| TestOutput::new("first response"))
881 .output(|| TestOutput::new("repeated response"))
882 .repeatedly()
883 .build();
884
885 let interceptor = MockResponseInterceptor::new()
886 .rule_mode(RuleMode::Sequential)
887 .with_rule(&rule);
888
889 let operation = create_test_operation(interceptor, false);
890
891 let result1 = operation
892 .invoke(TestInput::new("test-bucket", "test-key"))
893 .await;
894 assert!(result1.is_ok());
895 assert_eq!(result1.unwrap(), TestOutput::new("first response"));
896
897 for i in 0..10 {
899 let result = operation
900 .invoke(TestInput::new("test-bucket", "test-key"))
901 .await;
902 assert!(result.is_ok(), "Call {i} should succeed");
903 assert_eq!(result.unwrap(), TestOutput::new("repeated response"));
904 }
905 assert_eq!(rule.num_calls(), 11);
906 assert!(!rule.is_exhausted());
907 }
908
909 #[should_panic(expected = "times(n) called before adding a response to the sequence")]
910 #[test]
911 fn test_times_validation() {
912 let _rule = create_rule_builder()
914 .sequence()
915 .times(3)
916 .output(|| TestOutput::new("response"))
917 .build();
918 }
919
920 #[should_panic(expected = "repeatedly() called before adding a response to the sequence")]
921 #[test]
922 fn test_repeatedly_validation() {
923 let _rule = create_rule_builder().sequence().repeatedly().build();
925 }
926
927 #[test]
928 fn test_total_responses_overflow() {
929 let rule = create_rule_builder()
931 .sequence()
932 .output(|| TestOutput::new("response"))
933 .times(usize::MAX / 2)
934 .output(|| TestOutput::new("another response"))
935 .repeatedly()
936 .build();
937 assert_eq!(rule.max_responses, usize::MAX);
938 }
939
940 #[tokio::test]
941 async fn test_compute_response_conditional() {
942 use crate::MockResponse;
943
944 let rule = create_rule_builder().then_compute_response(|input| {
945 if input.key == "error-key" {
946 MockResponse::Error(TestError::new("conditional error"))
947 } else {
948 MockResponse::Output(TestOutput::new(&format!("response for {}", input.key)))
949 }
950 });
951
952 let interceptor = MockResponseInterceptor::new().with_rule(&rule);
953 let operation = create_test_operation(interceptor, false);
954
955 let result = operation
957 .invoke(TestInput::new("test-bucket", "success-key"))
958 .await;
959 assert!(result.is_ok());
960 assert_eq!(result.unwrap(), TestOutput::new("response for success-key"));
961
962 let result = operation
964 .invoke(TestInput::new("test-bucket", "error-key"))
965 .await;
966 assert!(result.is_err());
967 assert_eq!(rule.num_calls(), 2);
968 }
969}