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 BeforeSerializationInterceptorContextMut, 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::VecDeque;
20use std::fmt;
21use std::sync::{Arc, Mutex};
22
23#[derive(Debug, Clone)]
25struct ActiveRule(Rule);
26
27impl Storable for ActiveRule {
28 type Storer = StoreReplace<ActiveRule>;
29}
30
31pub struct MockResponseInterceptor {
33 rules: Arc<Mutex<VecDeque<Rule>>>,
34 rule_mode: RuleMode,
35 must_match: bool,
36 active_response: Arc<Mutex<Option<MockResponse<Output, Error>>>>,
37}
38
39impl fmt::Debug for MockResponseInterceptor {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 write!(f, "{} rules", self.rules.lock().unwrap().len())
42 }
43}
44
45impl Default for MockResponseInterceptor {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl MockResponseInterceptor {
52 pub fn new() -> Self {
56 Self {
57 rules: Default::default(),
58 rule_mode: RuleMode::MatchAny,
59 must_match: true,
60 active_response: Default::default(),
61 }
62 }
63 pub fn with_rule(self, rule: &Rule) -> Self {
67 self.rules.lock().unwrap().push_back(rule.clone());
68 self
69 }
70
71 pub fn rule_mode(mut self, rule_mode: RuleMode) -> Self {
75 self.rule_mode = rule_mode;
76 self
77 }
78
79 pub fn allow_passthrough(mut self) -> Self {
84 self.must_match = false;
85 self
86 }
87}
88
89impl Intercept for MockResponseInterceptor {
90 fn name(&self) -> &'static str {
91 "MockResponseInterceptor"
92 }
93
94 fn modify_before_serialization(
95 &self,
96 context: &mut BeforeSerializationInterceptorContextMut<'_>,
97 _runtime_components: &RuntimeComponents,
98 cfg: &mut ConfigBag,
99 ) -> Result<(), BoxError> {
100 let mut rules = self.rules.lock().unwrap();
101 let input = context.inner().input().expect("input set");
102
103 let mut matching_rule = None;
105 let mut matching_response = None;
106
107 match self.rule_mode {
108 RuleMode::Sequential => {
109 let i = 0;
111 while i < rules.len() && matching_response.is_none() {
112 let rule = &rules[i];
113
114 if rule.is_exhausted() || (rule.is_simple() && rule.num_calls() > 0) {
120 rules.remove(i);
122 continue; }
124
125 if !(rule.matcher)(input) {
127 panic!("In order matching was enforced but rule did not match {input:?}");
129 }
130
131 if let Some(response) = rule.next_response(input) {
133 matching_rule = Some(rule.clone());
134 matching_response = Some(response);
135 } else {
136 rules.remove(i);
138 continue; }
140
141 break;
143 }
144 }
145 RuleMode::MatchAny => {
146 for rule in rules.iter() {
148 if rule.is_exhausted() {
150 continue;
151 }
152
153 if (rule.matcher)(input) {
154 if let Some(response) = rule.next_response(input) {
155 matching_rule = Some(rule.clone());
156 matching_response = Some(response);
157 break;
158 }
159 }
160 }
161 }
162 };
163
164 match (matching_rule, matching_response) {
165 (Some(rule), Some(response)) => {
166 cfg.interceptor_state().store_put(ActiveRule(rule));
168 let mut active_resp = self.active_response.lock().unwrap();
171 let _ = (*active_resp).replace(response);
172 }
173 _ => {
174 if self.must_match {
176 panic!(
177 "must_match was enabled but no rules matched or all rules were exhausted for {input:?}"
178 );
179 }
180 }
181 }
182
183 Ok(())
184 }
185
186 fn modify_before_transmit(
187 &self,
188 context: &mut BeforeTransmitInterceptorContextMut<'_>,
189 _runtime_components: &RuntimeComponents,
190 cfg: &mut ConfigBag,
191 ) -> Result<(), BoxError> {
192 let mut state = self.active_response.lock().unwrap();
193 let mut active_response = (*state).take();
194 if active_response.is_none() {
195 if let Some(active_rule) = cfg.load::<ActiveRule>() {
197 let dummy_input = Input::doesnt_matter();
200 let next_resp = active_rule.0.next_response(&dummy_input);
201 active_response = next_resp;
202 }
203 }
204
205 if let Some(resp) = active_response {
206 match resp {
207 MockResponse::Http(http_resp) => {
209 context
210 .request_mut()
211 .add_extension(MockHttpResponse(Arc::new(http_resp)));
212 }
213 _ => {
214 let _ = (*state).replace(resp);
216 }
217 }
218 }
219
220 Ok(())
221 }
222
223 fn modify_before_attempt_completion(
224 &self,
225 context: &mut FinalizerInterceptorContextMut<'_>,
226 _runtime_components: &RuntimeComponents,
227 _cfg: &mut ConfigBag,
228 ) -> Result<(), BoxError> {
229 let mut state = self.active_response.lock().unwrap();
231 let active_response = (*state).take();
232 if let Some(resp) = active_response {
233 match resp {
234 MockResponse::Output(output) => {
235 context.inner_mut().set_output_or_error(Ok(output));
236 }
237 MockResponse::Error(error) => {
238 context
239 .inner_mut()
240 .set_output_or_error(Err(OrchestratorError::operation(error)));
241 }
242 MockResponse::Http(_) => {
243 }
245 }
246 }
247
248 Ok(())
249 }
250}
251
252#[derive(Clone)]
254struct MockHttpResponse(Arc<HttpResponse>);
255
256pub fn create_mock_http_client() -> SharedHttpClient {
258 infallible_client_fn(|mut req| {
259 if let Some(mock_response) = req.extensions_mut().remove::<MockHttpResponse>() {
261 let http_resp =
262 Arc::try_unwrap(mock_response.0).expect("mock HTTP response has single reference");
263 return http_resp.try_into_http1x().unwrap();
264 }
265
266 http::Response::builder()
268 .status(418)
269 .body(SdkBody::from("Mock HTTP client dummy response"))
270 .unwrap()
271 })
272}
273
274#[cfg(test)]
275mod tests {
276 use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep};
277 use aws_smithy_runtime::client::orchestrator::operation::Operation;
278 use aws_smithy_runtime::client::retries::classifiers::HttpStatusCodeClassifier;
279 use aws_smithy_runtime_api::client::orchestrator::{
280 HttpRequest, HttpResponse, OrchestratorError,
281 };
282 use aws_smithy_runtime_api::client::result::SdkError;
283 use aws_smithy_runtime_api::http::StatusCode;
284 use aws_smithy_types::body::SdkBody;
285 use aws_smithy_types::retry::RetryConfig;
286 use aws_smithy_types::timeout::TimeoutConfig;
287
288 use crate::{create_mock_http_client, MockResponseInterceptor, RuleBuilder, RuleMode};
289 use std::time::Duration;
290
291 #[derive(Debug)]
293 struct TestInput {
294 bucket: String,
295 key: String,
296 }
297 impl TestInput {
298 fn new(bucket: &str, key: &str) -> Self {
299 Self {
300 bucket: bucket.to_string(),
301 key: key.to_string(),
302 }
303 }
304 }
305
306 #[derive(Debug, PartialEq)]
307 struct TestOutput {
308 content: String,
309 }
310
311 impl TestOutput {
312 fn new(content: &str) -> Self {
313 Self {
314 content: content.to_string(),
315 }
316 }
317 }
318
319 #[derive(Debug)]
320 struct TestError {
321 message: String,
322 }
323
324 impl TestError {
325 fn new(message: &str) -> Self {
326 Self {
327 message: message.to_string(),
328 }
329 }
330 }
331
332 impl std::fmt::Display for TestError {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 write!(f, "{}", self.message)
335 }
336 }
337
338 impl std::error::Error for TestError {}
339
340 fn create_rule_builder() -> RuleBuilder<TestInput, TestOutput, TestError> {
342 RuleBuilder::new_from_mock(
343 || TestInput {
344 bucket: "".to_string(),
345 key: "".to_string(),
346 },
347 || {
348 let fut: std::future::Ready<Result<TestOutput, SdkError<TestError, HttpResponse>>> =
349 std::future::ready(Ok(TestOutput {
350 content: "".to_string(),
351 }));
352 fut
353 },
354 )
355 }
356
357 fn create_test_operation(
359 interceptor: MockResponseInterceptor,
360 enable_retries: bool,
361 ) -> Operation<TestInput, TestOutput, TestError> {
362 let builder = Operation::builder()
363 .service_name("test")
364 .operation_name("test")
365 .http_client(create_mock_http_client())
366 .endpoint_url("http://localhost:1234")
367 .no_auth()
368 .sleep_impl(SharedAsyncSleep::new(TokioSleep::new()))
369 .timeout_config(TimeoutConfig::disabled())
370 .interceptor(interceptor)
371 .serializer(|input: TestInput| {
372 let mut request = HttpRequest::new(SdkBody::empty());
373 request
374 .set_uri(format!("/{}/{}", input.bucket, input.key))
375 .expect("valid URI");
376 Ok(request)
377 })
378 .deserializer::<TestOutput, TestError>(|response| {
379 if response.status().is_success() {
380 let body = std::str::from_utf8(response.body().bytes().unwrap())
381 .unwrap_or("empty body")
382 .to_string();
383 Ok(TestOutput { content: body })
384 } else {
385 Err(OrchestratorError::operation(TestError {
386 message: format!("Error: {}", response.status()),
387 }))
388 }
389 });
390
391 if enable_retries {
392 let retry_config = RetryConfig::standard()
393 .with_max_attempts(5)
394 .with_initial_backoff(Duration::from_millis(1))
395 .with_max_backoff(Duration::from_millis(5));
396
397 builder
398 .retry_classifier(HttpStatusCodeClassifier::default())
399 .standard_retry(&retry_config)
400 .build()
401 } else {
402 builder.no_retry().build()
403 }
404 }
405
406 #[tokio::test]
407 async fn test_retry_sequence() {
408 let rule = create_rule_builder()
410 .match_requests(|input| input.bucket == "test-bucket" && input.key == "test-key")
411 .sequence()
412 .http_status(503, None)
413 .times(2)
414 .output(|| TestOutput::new("success after retries"))
415 .build();
416
417 let interceptor = MockResponseInterceptor::new()
419 .rule_mode(RuleMode::Sequential)
420 .with_rule(&rule);
421
422 let operation = create_test_operation(interceptor, true);
423
424 let result = operation
426 .invoke(TestInput::new("test-bucket", "test-key"))
427 .await;
428
429 assert!(
431 result.is_ok(),
432 "Expected success but got error: {:?}",
433 result.err()
434 );
435 assert_eq!(
436 result.unwrap(),
437 TestOutput {
438 content: "success after retries".to_string()
439 }
440 );
441
442 assert_eq!(rule.num_calls(), 3);
444 }
445
446 #[tokio::test]
447 async fn test_compute_output() {
448 let rule = create_rule_builder()
450 .match_requests(|input| input.bucket == "test-bucket" && input.key == "test-key")
451 .then_compute_output(|input| TestOutput {
452 content: format!("{}.{}", input.bucket, input.key),
453 });
454
455 let interceptor = MockResponseInterceptor::new()
457 .rule_mode(RuleMode::Sequential)
458 .with_rule(&rule);
459
460 let operation = create_test_operation(interceptor, true);
461
462 let result = operation
463 .invoke(TestInput::new("test-bucket", "test-key"))
464 .await;
465
466 assert!(
468 result.is_ok(),
469 "Expected success but got error: {:?}",
470 result.err()
471 );
472 assert_eq!(
473 result.unwrap(),
474 TestOutput {
475 content: "test-bucket.test-key".to_string()
476 }
477 );
478
479 assert_eq!(rule.num_calls(), 1);
481 }
482
483 #[should_panic(
484 expected = "must_match was enabled but no rules matched or all rules were exhausted for"
485 )]
486 #[tokio::test]
487 async fn test_exhausted_rules_sequential() {
488 let rule = create_rule_builder().then_output(|| TestOutput::new("only response"));
490
491 let interceptor = MockResponseInterceptor::new()
493 .rule_mode(RuleMode::Sequential)
494 .with_rule(&rule);
495
496 let operation = create_test_operation(interceptor, false);
497
498 let result1 = operation
500 .invoke(TestInput::new("test-bucket", "test-key"))
501 .await;
502 assert!(result1.is_ok());
503
504 let _result2 = operation
506 .invoke(TestInput::new("test-bucket", "test-key"))
507 .await;
508 }
509
510 #[tokio::test]
511 async fn test_rule_mode_match_any() {
512 let rule1 = create_rule_builder()
514 .match_requests(|input| input.bucket == "bucket1")
515 .then_output(|| TestOutput::new("response1"));
516
517 let rule2 = create_rule_builder()
518 .match_requests(|input| input.bucket == "bucket2")
519 .then_output(|| TestOutput::new("response2"));
520
521 let interceptor = MockResponseInterceptor::new()
523 .rule_mode(RuleMode::MatchAny)
524 .with_rule(&rule1)
525 .with_rule(&rule2);
526
527 let operation = create_test_operation(interceptor, false);
528
529 let result1 = operation
531 .invoke(TestInput::new("bucket1", "test-key"))
532 .await;
533 assert!(result1.is_ok());
534 assert_eq!(result1.unwrap(), TestOutput::new("response1"));
535
536 let result2 = operation
538 .invoke(TestInput::new("bucket2", "test-key"))
539 .await;
540 assert!(result2.is_ok());
541 assert_eq!(result2.unwrap(), TestOutput::new("response2"));
542
543 assert_eq!(rule1.num_calls(), 1);
545 assert_eq!(rule2.num_calls(), 1);
546
547 let result1 = operation
549 .invoke(TestInput::new("bucket1", "test-key"))
550 .await;
551 assert!(result1.is_ok());
552 assert_eq!(result1.unwrap(), TestOutput::new("response1"));
553 assert_eq!(rule1.num_calls(), 2);
554 }
555
556 #[tokio::test]
557 async fn test_mixed_response_types() {
558 let rule = create_rule_builder()
560 .sequence()
561 .output(|| TestOutput::new("first output"))
562 .error(|| TestError::new("expected error"))
563 .http_response(|| {
564 HttpResponse::new(
565 StatusCode::try_from(200).unwrap(),
566 SdkBody::from("http response"),
567 )
568 })
569 .build();
570
571 let interceptor = MockResponseInterceptor::new()
573 .rule_mode(RuleMode::Sequential)
574 .with_rule(&rule);
575
576 let operation = create_test_operation(interceptor, false);
577
578 let result1 = operation
580 .invoke(TestInput::new("test-bucket", "test-key"))
581 .await;
582 assert!(result1.is_ok());
583 assert_eq!(result1.unwrap(), TestOutput::new("first output"));
584
585 let result2 = operation
587 .invoke(TestInput::new("test-bucket", "test-key"))
588 .await;
589 assert!(result2.is_err());
590 let sdk_err = result2.unwrap_err();
591 let err = sdk_err.as_service_error().expect("expected service error");
592 assert_eq!(err.to_string(), "expected error");
593
594 let result3 = operation
596 .invoke(TestInput::new("test-bucket", "test-key"))
597 .await;
598 assert!(result3.is_ok());
599 assert_eq!(result3.unwrap(), TestOutput::new("http response"));
600
601 assert_eq!(rule.num_calls(), 3);
603 }
604 #[tokio::test]
605 async fn test_exhausted_sequence_match_any() {
606 let rule = create_rule_builder()
608 .match_requests(|input| input.bucket == "bucket-1")
609 .sequence()
610 .output(|| TestOutput::new("response 1"))
611 .output(|| TestOutput::new("response 2"))
612 .build();
613
614 let fallback_rule =
616 create_rule_builder().then_output(|| TestOutput::new("fallback response"));
617
618 let interceptor = MockResponseInterceptor::new()
620 .rule_mode(RuleMode::MatchAny)
621 .with_rule(&rule)
622 .with_rule(&fallback_rule);
623
624 let operation = create_test_operation(interceptor, false);
625
626 let result1 = operation
628 .invoke(TestInput::new("bucket-1", "test-key"))
629 .await;
630 assert!(result1.is_ok());
631 assert_eq!(result1.unwrap(), TestOutput::new("response 1"));
632
633 let result2 = operation
635 .invoke(TestInput::new("other-bucket", "test-key"))
636 .await;
637 assert!(result2.is_ok());
638 assert_eq!(result2.unwrap(), TestOutput::new("fallback response"));
639
640 let result3 = operation
642 .invoke(TestInput::new("bucket-1", "test-key"))
643 .await;
644 assert!(result3.is_ok());
645 assert_eq!(result3.unwrap(), TestOutput::new("response 2"));
646
647 let result4 = operation
649 .invoke(TestInput::new("bucket-1", "test-key"))
650 .await;
651 assert!(result4.is_ok());
652 assert_eq!(result4.unwrap(), TestOutput::new("fallback response"));
653
654 assert_eq!(rule.num_calls(), 2);
656 assert_eq!(fallback_rule.num_calls(), 2);
657 }
658
659 #[tokio::test]
660 async fn test_exhausted_sequence_sequential() {
661 let rule = create_rule_builder()
663 .sequence()
664 .output(|| TestOutput::new("response 1"))
665 .output(|| TestOutput::new("response 2"))
666 .build();
667
668 let fallback_rule =
670 create_rule_builder().then_output(|| TestOutput::new("fallback response"));
671
672 let interceptor = MockResponseInterceptor::new()
674 .rule_mode(RuleMode::Sequential)
675 .with_rule(&rule)
676 .with_rule(&fallback_rule);
677
678 let operation = create_test_operation(interceptor, false);
679
680 let result1 = operation
682 .invoke(TestInput::new("test-bucket", "test-key"))
683 .await;
684 assert!(result1.is_ok());
685 assert_eq!(result1.unwrap(), TestOutput::new("response 1"));
686
687 let result2 = operation
688 .invoke(TestInput::new("test-bucket", "test-key"))
689 .await;
690 assert!(result2.is_ok());
691 assert_eq!(result2.unwrap(), TestOutput::new("response 2"));
692
693 let result3 = operation
695 .invoke(TestInput::new("test-bucket", "test-key"))
696 .await;
697 assert!(result3.is_ok());
698 assert_eq!(result3.unwrap(), TestOutput::new("fallback response"));
699
700 assert_eq!(rule.num_calls(), 2);
702 assert_eq!(fallback_rule.num_calls(), 1);
703 }
704
705 #[tokio::test]
706 async fn test_concurrent_usage() {
707 use std::sync::Arc;
708 use tokio::task;
709
710 let rule = Arc::new(
712 create_rule_builder()
713 .sequence()
714 .output(|| TestOutput::new("response 1"))
715 .output(|| TestOutput::new("response 2"))
716 .output(|| TestOutput::new("response 3"))
717 .build(),
718 );
719
720 let interceptor = MockResponseInterceptor::new()
722 .rule_mode(RuleMode::Sequential)
723 .with_rule(&rule);
724
725 let operation = Arc::new(create_test_operation(interceptor, false));
726
727 let mut handles = vec![];
729 for i in 0..3 {
730 let op = operation.clone();
731 let handle = task::spawn(async move {
732 let result = op
733 .invoke(TestInput::new(&format!("bucket-{i}"), "test-key"))
734 .await;
735 result.unwrap()
736 });
737 handles.push(handle);
738 }
739
740 let mut results = vec![];
742 for handle in handles {
743 results.push(handle.await.unwrap());
744 }
745
746 results.sort_by(|a, b| a.content.cmp(&b.content));
748
749 assert_eq!(results.len(), 3);
751 assert_eq!(results[0], TestOutput::new("response 1"));
752 assert_eq!(results[1], TestOutput::new("response 2"));
753 assert_eq!(results[2], TestOutput::new("response 3"));
754
755 assert_eq!(rule.num_calls(), 3);
757 }
758
759 #[tokio::test]
760 async fn test_sequential_rule_removal() {
761 let rule1 = create_rule_builder()
763 .match_requests(|input| input.bucket == "test-bucket" && input.key != "correct-key")
764 .then_http_response(|| {
765 HttpResponse::new(
766 StatusCode::try_from(404).unwrap(),
767 SdkBody::from("not found"),
768 )
769 });
770
771 let rule2 = create_rule_builder()
773 .match_requests(|input| input.bucket == "test-bucket" && input.key == "correct-key")
774 .then_output(|| TestOutput::new("success"));
775
776 let interceptor = MockResponseInterceptor::new()
778 .rule_mode(RuleMode::Sequential)
779 .with_rule(&rule1)
780 .with_rule(&rule2);
781
782 let operation = create_test_operation(interceptor, true);
783
784 let result1 = operation.invoke(TestInput::new("test-bucket", "foo")).await;
786 assert!(result1.is_err());
787 assert_eq!(rule1.num_calls(), 1);
788
789 let result2 = operation
792 .invoke(TestInput::new("test-bucket", "correct-key"))
793 .await;
794
795 assert!(result2.is_ok());
797 assert_eq!(result2.unwrap(), TestOutput::new("success"));
798 assert_eq!(rule2.num_calls(), 1);
799 }
800
801 #[tokio::test]
802 async fn test_simple_rule_in_match_any_mode() {
803 let rule = create_rule_builder().then_output(|| TestOutput::new("simple response"));
804
805 let interceptor = MockResponseInterceptor::new()
806 .rule_mode(RuleMode::MatchAny)
807 .with_rule(&rule);
808
809 let operation = create_test_operation(interceptor, false);
810
811 for i in 0..5 {
812 let result = operation
813 .invoke(TestInput::new("test-bucket", "test-key"))
814 .await;
815 assert!(result.is_ok(), "Call {i} should succeed");
816 assert_eq!(result.unwrap(), TestOutput::new("simple response"));
817 }
818 assert_eq!(rule.num_calls(), 5);
819 assert!(!rule.is_exhausted());
820 }
821
822 #[tokio::test]
823 async fn test_simple_rule_in_sequential_mode() {
824 let rule1 = create_rule_builder().then_output(|| TestOutput::new("first response"));
825 let rule2 = create_rule_builder().then_output(|| TestOutput::new("second response"));
826
827 let interceptor = MockResponseInterceptor::new()
828 .rule_mode(RuleMode::Sequential)
829 .with_rule(&rule1)
830 .with_rule(&rule2);
831
832 let operation = create_test_operation(interceptor, false);
833
834 let result1 = operation
835 .invoke(TestInput::new("test-bucket", "test-key"))
836 .await;
837 assert!(result1.is_ok());
838 assert_eq!(result1.unwrap(), TestOutput::new("first response"));
839
840 let result2 = operation
842 .invoke(TestInput::new("test-bucket", "test-key"))
843 .await;
844 assert!(result2.is_ok());
845 assert_eq!(result2.unwrap(), TestOutput::new("second response"));
846
847 assert_eq!(rule1.num_calls(), 1);
848 assert_eq!(rule2.num_calls(), 1);
849 }
850
851 #[tokio::test]
852 async fn test_repeatedly_method() {
853 let rule = create_rule_builder()
854 .sequence()
855 .output(|| TestOutput::new("first response"))
856 .output(|| TestOutput::new("repeated response"))
857 .repeatedly()
858 .build();
859
860 let interceptor = MockResponseInterceptor::new()
861 .rule_mode(RuleMode::Sequential)
862 .with_rule(&rule);
863
864 let operation = create_test_operation(interceptor, false);
865
866 let result1 = operation
867 .invoke(TestInput::new("test-bucket", "test-key"))
868 .await;
869 assert!(result1.is_ok());
870 assert_eq!(result1.unwrap(), TestOutput::new("first response"));
871
872 for i in 0..10 {
874 let result = operation
875 .invoke(TestInput::new("test-bucket", "test-key"))
876 .await;
877 assert!(result.is_ok(), "Call {i} should succeed");
878 assert_eq!(result.unwrap(), TestOutput::new("repeated response"));
879 }
880 assert_eq!(rule.num_calls(), 11);
881 assert!(!rule.is_exhausted());
882 }
883
884 #[should_panic(expected = "times(n) called before adding a response to the sequence")]
885 #[test]
886 fn test_times_validation() {
887 let _rule = create_rule_builder()
889 .sequence()
890 .times(3)
891 .output(|| TestOutput::new("response"))
892 .build();
893 }
894
895 #[should_panic(expected = "repeatedly() called before adding a response to the sequence")]
896 #[test]
897 fn test_repeatedly_validation() {
898 let _rule = create_rule_builder().sequence().repeatedly().build();
900 }
901
902 #[test]
903 fn test_total_responses_overflow() {
904 let rule = create_rule_builder()
906 .sequence()
907 .output(|| TestOutput::new("response"))
908 .times(usize::MAX / 2)
909 .output(|| TestOutput::new("another response"))
910 .repeatedly()
911 .build();
912 assert_eq!(rule.max_responses, usize::MAX);
913 }
914}