1use crate::error::{AptosError, AptosResult};
7use crate::retry::RetryConfig;
8use crate::types::ChainId;
9use std::time::Duration;
10use url::Url;
11
12pub fn validate_url_scheme(url: &Url) -> AptosResult<()> {
24 match url.scheme() {
25 "https" => Ok(()),
26 "http" => {
27 Ok(())
29 }
30 scheme => Err(AptosError::Config(format!(
31 "unsupported URL scheme '{scheme}': only 'http' and 'https' are allowed"
32 ))),
33 }
34}
35
36pub async fn read_response_bounded(
53 mut response: reqwest::Response,
54 max_size: usize,
55) -> AptosResult<Vec<u8>> {
56 if let Some(content_length) = response.content_length()
58 && content_length > max_size as u64
59 {
60 return Err(AptosError::Api {
61 status_code: response.status().as_u16(),
62 message: format!(
63 "response too large: Content-Length {content_length} bytes exceeds limit of {max_size} bytes"
64 ),
65 error_code: Some("RESPONSE_TOO_LARGE".into()),
66 vm_error_code: None,
67 });
68 }
69
70 let mut body = Vec::with_capacity(std::cmp::min(max_size, 1024 * 1024));
73 while let Some(chunk) = response.chunk().await? {
74 if body.len().saturating_add(chunk.len()) > max_size {
75 return Err(AptosError::Api {
76 status_code: response.status().as_u16(),
77 message: format!(
78 "response too large: exceeded limit of {max_size} bytes during streaming"
79 ),
80 error_code: Some("RESPONSE_TOO_LARGE".into()),
81 vm_error_code: None,
82 });
83 }
84 body.extend_from_slice(&chunk);
85 }
86
87 Ok(body)
88}
89
90#[derive(Debug, Clone)]
94pub struct PoolConfig {
95 pub max_idle_per_host: Option<usize>,
98 pub max_idle_total: usize,
101 pub idle_timeout: Duration,
104 pub tcp_keepalive: Option<Duration>,
107 pub tcp_nodelay: bool,
110 pub max_response_size: usize,
118}
119
120const DEFAULT_MAX_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
130
131impl Default for PoolConfig {
132 fn default() -> Self {
133 Self {
134 max_idle_per_host: None, max_idle_total: 100,
136 idle_timeout: Duration::from_secs(90),
137 tcp_keepalive: Some(Duration::from_mins(1)),
138 tcp_nodelay: true,
139 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
140 }
141 }
142}
143
144impl PoolConfig {
145 pub fn builder() -> PoolConfigBuilder {
147 PoolConfigBuilder::default()
148 }
149
150 pub fn high_throughput() -> Self {
156 Self {
157 max_idle_per_host: Some(32),
158 max_idle_total: 256,
159 idle_timeout: Duration::from_mins(5),
160 tcp_keepalive: Some(Duration::from_secs(30)),
161 tcp_nodelay: true,
162 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
163 }
164 }
165
166 pub fn low_latency() -> Self {
172 Self {
173 max_idle_per_host: Some(8),
174 max_idle_total: 32,
175 idle_timeout: Duration::from_secs(30),
176 tcp_keepalive: Some(Duration::from_secs(15)),
177 tcp_nodelay: true,
178 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
179 }
180 }
181
182 pub fn minimal() -> Self {
187 Self {
188 max_idle_per_host: Some(2),
189 max_idle_total: 8,
190 idle_timeout: Duration::from_secs(10),
191 tcp_keepalive: None,
192 tcp_nodelay: true,
193 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
194 }
195 }
196}
197
198#[derive(Debug, Clone, Default)]
200#[allow(clippy::option_option)] pub struct PoolConfigBuilder {
202 max_idle_per_host: Option<usize>,
203 max_idle_total: Option<usize>,
204 idle_timeout: Option<Duration>,
205 tcp_keepalive: Option<Option<Duration>>,
207 tcp_nodelay: Option<bool>,
208 max_response_size: Option<usize>,
209}
210
211impl PoolConfigBuilder {
212 #[must_use]
214 pub fn max_idle_per_host(mut self, max: usize) -> Self {
215 self.max_idle_per_host = Some(max);
216 self
217 }
218
219 #[must_use]
221 pub fn unlimited_idle_per_host(mut self) -> Self {
222 self.max_idle_per_host = None;
223 self
224 }
225
226 #[must_use]
228 pub fn max_idle_total(mut self, max: usize) -> Self {
229 self.max_idle_total = Some(max);
230 self
231 }
232
233 #[must_use]
235 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
236 self.idle_timeout = Some(timeout);
237 self
238 }
239
240 #[must_use]
242 pub fn tcp_keepalive(mut self, interval: Duration) -> Self {
243 self.tcp_keepalive = Some(Some(interval));
244 self
245 }
246
247 #[must_use]
249 pub fn no_tcp_keepalive(mut self) -> Self {
250 self.tcp_keepalive = Some(None);
251 self
252 }
253
254 #[must_use]
256 pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
257 self.tcp_nodelay = Some(enabled);
258 self
259 }
260
261 #[must_use]
267 pub fn max_response_size(mut self, size: usize) -> Self {
268 self.max_response_size = Some(size);
269 self
270 }
271
272 pub fn build(self) -> PoolConfig {
274 let default = PoolConfig::default();
275 PoolConfig {
276 max_idle_per_host: self.max_idle_per_host.or(default.max_idle_per_host),
277 max_idle_total: self.max_idle_total.unwrap_or(default.max_idle_total),
278 idle_timeout: self.idle_timeout.unwrap_or(default.idle_timeout),
279 tcp_keepalive: self.tcp_keepalive.unwrap_or(default.tcp_keepalive),
280 tcp_nodelay: self.tcp_nodelay.unwrap_or(default.tcp_nodelay),
281 max_response_size: self.max_response_size.unwrap_or(default.max_response_size),
282 }
283 }
284}
285
286#[derive(Debug, Clone)]
309pub struct AptosConfig {
310 pub(crate) network: Network,
312 pub(crate) fullnode_url: Url,
314 pub(crate) indexer_url: Option<Url>,
316 pub(crate) faucet_url: Option<Url>,
318 pub(crate) timeout: Duration,
320 pub(crate) retry_config: RetryConfig,
322 pub(crate) pool_config: PoolConfig,
324 pub(crate) api_key: Option<String>,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
330pub enum Network {
331 Mainnet,
333 Testnet,
335 Devnet,
337 Local,
339 Custom,
341}
342
343impl Network {
344 pub fn chain_id(&self) -> ChainId {
351 match self {
352 Network::Mainnet => ChainId::mainnet(),
353 Network::Testnet => ChainId::testnet(),
354 Network::Devnet => ChainId::new(0),
355 Network::Local => ChainId::new(4),
356 Network::Custom => ChainId::new(0),
357 }
358 }
359
360 pub fn as_str(&self) -> &'static str {
362 match self {
363 Network::Mainnet => "mainnet",
364 Network::Testnet => "testnet",
365 Network::Devnet => "devnet",
366 Network::Local => "local",
367 Network::Custom => "custom",
368 }
369 }
370
371 #[must_use]
379 pub fn pepper_url(self) -> Option<&'static str> {
380 match self {
381 Network::Mainnet => Some("https://api.mainnet.aptoslabs.com/keyless/pepper/v0"),
382 Network::Testnet => Some("https://api.testnet.aptoslabs.com/keyless/pepper/v0"),
383 Network::Devnet => Some("https://api.devnet.aptoslabs.com/keyless/pepper/v0"),
384 Network::Local => Some("https://api.devnet.aptoslabs.com/keyless/pepper/v0"),
386 Network::Custom => None,
387 }
388 }
389
390 #[must_use]
398 pub fn prover_url(self) -> Option<&'static str> {
399 match self {
400 Network::Mainnet => Some("https://api.mainnet.aptoslabs.com/keyless/prover/v0"),
401 Network::Testnet => Some("https://api.testnet.aptoslabs.com/keyless/prover/v0"),
402 Network::Devnet => Some("https://api.devnet.aptoslabs.com/keyless/prover/v0"),
403 Network::Local => Some("https://api.devnet.aptoslabs.com/keyless/prover/v0"),
404 Network::Custom => None,
405 }
406 }
407}
408
409impl Default for AptosConfig {
410 fn default() -> Self {
411 Self::devnet()
412 }
413}
414
415impl AptosConfig {
416 #[allow(clippy::missing_panics_doc)]
426 #[must_use]
427 pub fn mainnet() -> Self {
428 Self {
429 network: Network::Mainnet,
430 fullnode_url: Url::parse("https://fullnode.mainnet.aptoslabs.com/v1")
431 .expect("valid mainnet URL"),
432 indexer_url: Some(
433 Url::parse("https://indexer.mainnet.aptoslabs.com/v1/graphql")
434 .expect("valid indexer URL"),
435 ),
436 faucet_url: None, timeout: Duration::from_secs(30),
438 retry_config: RetryConfig::conservative(), pool_config: PoolConfig::default(),
440 api_key: None,
441 }
442 }
443
444 #[allow(clippy::missing_panics_doc)]
454 #[must_use]
455 pub fn testnet() -> Self {
456 Self {
457 network: Network::Testnet,
458 fullnode_url: Url::parse("https://fullnode.testnet.aptoslabs.com/v1")
459 .expect("valid testnet URL"),
460 indexer_url: Some(
461 Url::parse("https://indexer.testnet.aptoslabs.com/v1/graphql")
462 .expect("valid indexer URL"),
463 ),
464 faucet_url: Some(
465 Url::parse("https://faucet.testnet.aptoslabs.com").expect("valid faucet URL"),
466 ),
467 timeout: Duration::from_secs(30),
468 retry_config: RetryConfig::default(),
469 pool_config: PoolConfig::default(),
470 api_key: None,
471 }
472 }
473
474 #[allow(clippy::missing_panics_doc)]
484 #[must_use]
485 pub fn devnet() -> Self {
486 Self {
487 network: Network::Devnet,
488 fullnode_url: Url::parse("https://fullnode.devnet.aptoslabs.com/v1")
489 .expect("valid devnet URL"),
490 indexer_url: Some(
491 Url::parse("https://indexer.devnet.aptoslabs.com/v1/graphql")
492 .expect("valid indexer URL"),
493 ),
494 faucet_url: Some(
495 Url::parse("https://faucet.devnet.aptoslabs.com").expect("valid faucet URL"),
496 ),
497 timeout: Duration::from_secs(30),
498 retry_config: RetryConfig::default(),
499 pool_config: PoolConfig::default(),
500 api_key: None,
501 }
502 }
503
504 #[allow(clippy::missing_panics_doc)]
517 #[must_use]
518 pub fn local() -> Self {
519 Self {
520 network: Network::Local,
521 fullnode_url: Url::parse("http://127.0.0.1:8080/v1").expect("valid local URL"),
522 indexer_url: None,
523 faucet_url: Some(Url::parse("http://127.0.0.1:8081").expect("valid local faucet URL")),
524 timeout: Duration::from_secs(10),
525 retry_config: RetryConfig::aggressive(), pool_config: PoolConfig::low_latency(), api_key: None,
528 }
529 }
530
531 pub fn custom(fullnode_url: &str) -> AptosResult<Self> {
552 let url = Url::parse(fullnode_url)?;
553 validate_url_scheme(&url)?;
554 Ok(Self {
555 network: Network::Custom,
556 fullnode_url: url,
557 indexer_url: None,
558 faucet_url: None,
559 timeout: Duration::from_secs(30),
560 retry_config: RetryConfig::default(),
561 pool_config: PoolConfig::default(),
562 api_key: None,
563 })
564 }
565
566 #[must_use]
568 pub fn with_timeout(mut self, timeout: Duration) -> Self {
569 self.timeout = timeout;
570 self
571 }
572
573 #[must_use]
585 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
586 self.retry_config = retry_config;
587 self
588 }
589
590 #[must_use]
594 pub fn without_retry(mut self) -> Self {
595 self.retry_config = RetryConfig::no_retry();
596 self
597 }
598
599 #[must_use]
603 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
604 self.retry_config = RetryConfig::builder()
605 .max_retries(max_retries)
606 .initial_delay_ms(self.retry_config.initial_delay_ms)
607 .max_delay_ms(self.retry_config.max_delay_ms)
608 .exponential_base(self.retry_config.exponential_base)
609 .jitter(self.retry_config.jitter)
610 .build();
611 self
612 }
613
614 #[must_use]
626 pub fn with_pool(mut self, pool_config: PoolConfig) -> Self {
627 self.pool_config = pool_config;
628 self
629 }
630
631 #[must_use]
636 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
637 self.api_key = Some(api_key.into());
638 self
639 }
640
641 pub fn with_indexer_url(mut self, url: &str) -> AptosResult<Self> {
652 let parsed = Url::parse(url)?;
653 validate_url_scheme(&parsed)?;
654 self.indexer_url = Some(parsed);
655 Ok(self)
656 }
657
658 pub fn with_faucet_url(mut self, url: &str) -> AptosResult<Self> {
669 let parsed = Url::parse(url)?;
670 validate_url_scheme(&parsed)?;
671 self.faucet_url = Some(parsed);
672 Ok(self)
673 }
674
675 pub fn network(&self) -> Network {
677 self.network
678 }
679
680 pub fn fullnode_url(&self) -> &Url {
682 &self.fullnode_url
683 }
684
685 pub fn indexer_url(&self) -> Option<&Url> {
687 self.indexer_url.as_ref()
688 }
689
690 pub fn faucet_url(&self) -> Option<&Url> {
692 self.faucet_url.as_ref()
693 }
694
695 pub fn chain_id(&self) -> ChainId {
697 self.network.chain_id()
698 }
699
700 pub fn retry_config(&self) -> &RetryConfig {
702 &self.retry_config
703 }
704
705 pub fn timeout(&self) -> Duration {
707 self.timeout
708 }
709
710 pub fn pool_config(&self) -> &PoolConfig {
712 &self.pool_config
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719
720 #[test]
721 fn test_mainnet_config() {
722 let config = AptosConfig::mainnet();
723 assert_eq!(config.network(), Network::Mainnet);
724 assert!(config.fullnode_url().as_str().contains("mainnet"));
725 assert!(config.faucet_url().is_none());
726 }
727
728 #[test]
729 fn test_testnet_config() {
730 let config = AptosConfig::testnet();
731 assert_eq!(config.network(), Network::Testnet);
732 assert!(config.fullnode_url().as_str().contains("testnet"));
733 assert!(config.faucet_url().is_some());
734 }
735
736 #[test]
737 fn test_devnet_config() {
738 let config = AptosConfig::devnet();
739 assert_eq!(config.network(), Network::Devnet);
740 assert!(config.fullnode_url().as_str().contains("devnet"));
741 assert!(config.faucet_url().is_some());
742 assert!(config.indexer_url().is_some());
743 }
744
745 #[test]
746 fn test_local_config() {
747 let config = AptosConfig::local();
748 assert_eq!(config.network(), Network::Local);
749 assert!(config.fullnode_url().as_str().contains("127.0.0.1"));
750 assert!(config.faucet_url().is_some());
751 assert!(config.indexer_url().is_none());
752 }
753
754 #[test]
755 fn test_custom_config() {
756 let config = AptosConfig::custom("https://custom.example.com/v1").unwrap();
757 assert_eq!(config.network(), Network::Custom);
758 assert_eq!(
759 config.fullnode_url().as_str(),
760 "https://custom.example.com/v1"
761 );
762 }
763
764 #[test]
765 fn test_custom_config_invalid_url() {
766 let result = AptosConfig::custom("not a valid url");
767 assert!(result.is_err());
768 }
769
770 #[test]
771 fn test_builder_methods() {
772 let config = AptosConfig::testnet()
773 .with_timeout(Duration::from_mins(1))
774 .with_max_retries(5)
775 .with_api_key("test-key");
776
777 assert_eq!(config.timeout, Duration::from_mins(1));
778 assert_eq!(config.retry_config.max_retries, 5);
779 assert_eq!(config.api_key, Some("test-key".to_string()));
780 }
781
782 #[test]
783 fn test_retry_config() {
784 let config = AptosConfig::testnet().with_retry(RetryConfig::aggressive());
785
786 assert_eq!(config.retry_config.max_retries, 5);
787 assert_eq!(config.retry_config.initial_delay_ms, 50);
788
789 let config = AptosConfig::testnet().without_retry();
790 assert_eq!(config.retry_config.max_retries, 0);
791 }
792
793 #[test]
794 fn test_network_retry_defaults() {
795 let mainnet = AptosConfig::mainnet();
797 assert_eq!(mainnet.retry_config.max_retries, 3);
798
799 let local = AptosConfig::local();
801 assert_eq!(local.retry_config.max_retries, 5);
802 }
803
804 #[test]
805 fn test_pool_config_default() {
806 let config = PoolConfig::default();
807 assert_eq!(config.max_idle_total, 100);
808 assert_eq!(config.idle_timeout, Duration::from_secs(90));
809 assert!(config.tcp_nodelay);
810 }
811
812 #[test]
813 fn test_pool_config_presets() {
814 let high = PoolConfig::high_throughput();
815 assert_eq!(high.max_idle_per_host, Some(32));
816 assert_eq!(high.max_idle_total, 256);
817
818 let low = PoolConfig::low_latency();
819 assert_eq!(low.max_idle_per_host, Some(8));
820 assert_eq!(low.idle_timeout, Duration::from_secs(30));
821
822 let minimal = PoolConfig::minimal();
823 assert_eq!(minimal.max_idle_per_host, Some(2));
824 assert_eq!(minimal.max_idle_total, 8);
825 }
826
827 #[test]
828 fn test_pool_config_builder() {
829 let config = PoolConfig::builder()
830 .max_idle_per_host(16)
831 .max_idle_total(64)
832 .idle_timeout(Duration::from_mins(1))
833 .tcp_nodelay(false)
834 .build();
835
836 assert_eq!(config.max_idle_per_host, Some(16));
837 assert_eq!(config.max_idle_total, 64);
838 assert_eq!(config.idle_timeout, Duration::from_mins(1));
839 assert!(!config.tcp_nodelay);
840 }
841
842 #[test]
843 fn test_pool_config_builder_tcp_keepalive() {
844 let config = PoolConfig::builder()
845 .tcp_keepalive(Duration::from_secs(30))
846 .build();
847 assert_eq!(config.tcp_keepalive, Some(Duration::from_secs(30)));
848
849 let config = PoolConfig::builder().no_tcp_keepalive().build();
850 assert_eq!(config.tcp_keepalive, None);
851 }
852
853 #[test]
854 fn test_pool_config_builder_unlimited_idle() {
855 let config = PoolConfig::builder().unlimited_idle_per_host().build();
856 assert_eq!(config.max_idle_per_host, None);
857 }
858
859 #[test]
860 fn test_aptos_config_with_pool() {
861 let config = AptosConfig::testnet().with_pool(PoolConfig::high_throughput());
862
863 assert_eq!(config.pool_config.max_idle_total, 256);
864 }
865
866 #[test]
867 fn test_aptos_config_with_indexer_url() {
868 let config = AptosConfig::testnet()
869 .with_indexer_url("https://custom-indexer.example.com/graphql")
870 .unwrap();
871 assert_eq!(
872 config.indexer_url().unwrap().as_str(),
873 "https://custom-indexer.example.com/graphql"
874 );
875 }
876
877 #[test]
878 fn test_aptos_config_with_faucet_url() {
879 let config = AptosConfig::mainnet()
880 .with_faucet_url("https://custom-faucet.example.com")
881 .unwrap();
882 assert_eq!(
883 config.faucet_url().unwrap().as_str(),
884 "https://custom-faucet.example.com/"
885 );
886 }
887
888 #[test]
889 fn test_aptos_config_default() {
890 let config = AptosConfig::default();
891 assert_eq!(config.network(), Network::Devnet);
892 }
893
894 #[test]
895 fn test_network_chain_id() {
896 assert_eq!(Network::Mainnet.chain_id().id(), 1);
897 assert_eq!(Network::Testnet.chain_id().id(), 2);
898 assert_eq!(Network::Devnet.chain_id().id(), 0);
901 assert_eq!(Network::Local.chain_id().id(), 4);
902 assert_eq!(Network::Custom.chain_id().id(), 0);
903 }
904
905 #[test]
906 fn test_network_as_str() {
907 assert_eq!(Network::Mainnet.as_str(), "mainnet");
908 assert_eq!(Network::Testnet.as_str(), "testnet");
909 assert_eq!(Network::Devnet.as_str(), "devnet");
910 assert_eq!(Network::Local.as_str(), "local");
911 assert_eq!(Network::Custom.as_str(), "custom");
912 }
913
914 #[test]
915 fn test_network_keyless_service_urls() {
916 assert_eq!(
917 Network::Mainnet.pepper_url(),
918 Some("https://api.mainnet.aptoslabs.com/keyless/pepper/v0")
919 );
920 assert_eq!(
921 Network::Testnet.pepper_url(),
922 Some("https://api.testnet.aptoslabs.com/keyless/pepper/v0")
923 );
924 assert_eq!(
925 Network::Devnet.pepper_url(),
926 Some("https://api.devnet.aptoslabs.com/keyless/pepper/v0")
927 );
928 assert_eq!(
929 Network::Local.pepper_url(),
930 Some("https://api.devnet.aptoslabs.com/keyless/pepper/v0")
931 );
932 assert_eq!(Network::Custom.pepper_url(), None);
933
934 assert_eq!(
935 Network::Mainnet.prover_url(),
936 Some("https://api.mainnet.aptoslabs.com/keyless/prover/v0")
937 );
938 assert_eq!(
939 Network::Testnet.prover_url(),
940 Some("https://api.testnet.aptoslabs.com/keyless/prover/v0")
941 );
942 assert_eq!(
943 Network::Devnet.prover_url(),
944 Some("https://api.devnet.aptoslabs.com/keyless/prover/v0")
945 );
946 assert_eq!(
947 Network::Local.prover_url(),
948 Some("https://api.devnet.aptoslabs.com/keyless/prover/v0")
949 );
950 assert_eq!(Network::Custom.prover_url(), None);
951 }
952
953 #[test]
954 fn test_aptos_config_getters() {
955 let config = AptosConfig::testnet();
956
957 assert_eq!(config.timeout(), Duration::from_secs(30));
958 assert!(config.retry_config().max_retries > 0);
959 assert!(config.pool_config().max_idle_total > 0);
960 assert_eq!(config.chain_id().id(), 2);
961 }
962
963 #[tokio::test]
964 async fn test_read_response_bounded_normal() {
965 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
966 let server = MockServer::start().await;
967 Mock::given(method("GET"))
968 .respond_with(ResponseTemplate::new(200).set_body_string("hello world"))
969 .mount(&server)
970 .await;
971
972 let response = reqwest::get(server.uri()).await.unwrap();
973 let body = read_response_bounded(response, 1024).await.unwrap();
974 assert_eq!(body, b"hello world");
975 }
976
977 #[tokio::test]
978 async fn test_read_response_bounded_rejects_oversized_content_length() {
979 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
980 let server = MockServer::start().await;
981 let body = "x".repeat(200);
985 Mock::given(method("GET"))
986 .respond_with(ResponseTemplate::new(200).set_body_string(body))
987 .mount(&server)
988 .await;
989
990 let response = reqwest::get(server.uri()).await.unwrap();
991 let result = read_response_bounded(response, 100).await;
993 assert!(result.is_err());
994 let err = result.unwrap_err().to_string();
995 assert!(err.contains("response too large"));
996 }
997
998 #[tokio::test]
999 async fn test_read_response_bounded_rejects_oversized_body() {
1000 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
1001 let server = MockServer::start().await;
1002 let large_body = "x".repeat(500);
1003 Mock::given(method("GET"))
1004 .respond_with(ResponseTemplate::new(200).set_body_string(large_body))
1005 .mount(&server)
1006 .await;
1007
1008 let response = reqwest::get(server.uri()).await.unwrap();
1009 let result = read_response_bounded(response, 100).await;
1010 assert!(result.is_err());
1011 }
1012
1013 #[tokio::test]
1014 async fn test_read_response_bounded_exact_limit() {
1015 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
1016 let server = MockServer::start().await;
1017 let body = "x".repeat(100);
1018 Mock::given(method("GET"))
1019 .respond_with(ResponseTemplate::new(200).set_body_string(body.clone()))
1020 .mount(&server)
1021 .await;
1022
1023 let response = reqwest::get(server.uri()).await.unwrap();
1024 let result = read_response_bounded(response, 100).await.unwrap();
1025 assert_eq!(result.len(), 100);
1026 }
1027
1028 #[tokio::test]
1029 async fn test_read_response_bounded_empty() {
1030 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
1031 let server = MockServer::start().await;
1032 Mock::given(method("GET"))
1033 .respond_with(ResponseTemplate::new(200))
1034 .mount(&server)
1035 .await;
1036
1037 let response = reqwest::get(server.uri()).await.unwrap();
1038 let result = read_response_bounded(response, 1024).await.unwrap();
1039 assert!(result.is_empty());
1040 }
1041}