cloud_sdk_testkit/
mock.rs1use core::fmt;
4
5use cloud_sdk::Method;
6use cloud_sdk::transport::{
7 AsyncTransport, BlockingTransport, RequestTarget, TransportRequest, TransportResponse,
8};
9
10use crate::{FixtureBodyError, ResponseFixture};
11
12#[derive(Clone, Copy, Eq, PartialEq)]
14pub struct ExpectedRequest<'a> {
15 method: Method,
16 target: RequestTarget<'a>,
17 body: &'a [u8],
18}
19
20impl<'a> ExpectedRequest<'a> {
21 #[must_use]
23 pub const fn new(method: Method, target: RequestTarget<'a>) -> Self {
24 Self {
25 method,
26 target,
27 body: &[],
28 }
29 }
30
31 #[must_use]
33 pub const fn with_body(mut self, body: &'a [u8]) -> Self {
34 self.body = body;
35 self
36 }
37
38 const fn method(self) -> Method {
39 self.method
40 }
41
42 const fn target(self) -> RequestTarget<'a> {
43 self.target
44 }
45
46 const fn body(self) -> &'a [u8] {
47 self.body
48 }
49}
50
51impl fmt::Debug for ExpectedRequest<'_> {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 formatter
54 .debug_struct("ExpectedRequest")
55 .field("method", &self.method)
56 .field("target", &"[redacted]")
57 .field("body", &"[redacted]")
58 .finish()
59 }
60}
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub struct MockExchange<'a> {
65 request: ExpectedRequest<'a>,
66 response: ResponseFixture<'a>,
67}
68
69impl<'a> MockExchange<'a> {
70 #[must_use]
72 pub const fn new(request: ExpectedRequest<'a>, response: ResponseFixture<'a>) -> Self {
73 Self { request, response }
74 }
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum MockError {
80 Exhausted,
82 MethodMismatch,
84 TargetMismatch,
86 BodyMismatch,
88 ResponseBufferTooSmall,
90 CursorOverflow,
92 InvalidFixtureMetadata,
94}
95
96impl_static_error!(MockError,
97 Self::Exhausted => "mock transport has no expected exchange remaining",
98 Self::MethodMismatch => "mock request method differs from expectation",
99 Self::TargetMismatch => "mock request target differs from expectation",
100 Self::BodyMismatch => "mock request body differs from expectation",
101 Self::ResponseBufferTooSmall => "mock response buffer is too small",
102 Self::CursorOverflow => "mock transport cursor overflowed",
103 Self::InvalidFixtureMetadata => "mock fixture metadata is invalid",
104);
105
106pub struct MockTransport<'a> {
108 exchanges: &'a [MockExchange<'a>],
109 cursor: usize,
110}
111
112impl<'a> MockTransport<'a> {
113 #[must_use]
115 pub const fn new(exchanges: &'a [MockExchange<'a>]) -> Self {
116 Self {
117 exchanges,
118 cursor: 0,
119 }
120 }
121
122 #[must_use]
124 pub const fn remaining(&self) -> usize {
125 self.exchanges.len().saturating_sub(self.cursor)
126 }
127
128 #[must_use]
130 pub const fn is_complete(&self) -> bool {
131 self.remaining() == 0
132 }
133
134 fn send_inner<'buffer>(
135 &mut self,
136 request: TransportRequest<'_>,
137 response_body: &'buffer mut [u8],
138 ) -> Result<TransportResponse<'buffer>, MockError> {
139 let exchange = self
140 .exchanges
141 .get(self.cursor)
142 .ok_or(MockError::Exhausted)?;
143 if request.method() != exchange.request.method() {
144 return Err(MockError::MethodMismatch);
145 }
146 if request.target() != exchange.request.target() {
147 return Err(MockError::TargetMismatch);
148 }
149 if request.body() != exchange.request.body() {
150 return Err(MockError::BodyMismatch);
151 }
152 let next_cursor = self
153 .cursor
154 .checked_add(1)
155 .ok_or(MockError::CursorOverflow)?;
156 let body_len =
157 exchange
158 .response
159 .body()
160 .write_to(response_body)
161 .map_err(|error| match error {
162 FixtureBodyError::OutputTooSmall | FixtureBodyError::TooLarge => {
163 MockError::ResponseBufferTooSmall
164 }
165 })?;
166 let initialized = response_body
167 .get(..body_len)
168 .ok_or(MockError::ResponseBufferTooSmall)?;
169 self.cursor = next_cursor;
170 let response = TransportResponse::new(exchange.response.status(), initialized);
171 let rate_limit = exchange
172 .response
173 .rate_limit()
174 .map(|value| value.into_rate_limit())
175 .transpose()
176 .map_err(|_| MockError::InvalidFixtureMetadata)?;
177 Ok(rate_limit.map_or(response, |value| response.with_rate_limit(value)))
178 }
179}
180
181impl BlockingTransport for MockTransport<'_> {
182 type Error = MockError;
183
184 fn send<'buffer>(
185 &mut self,
186 request: TransportRequest<'_>,
187 response_body: &'buffer mut [u8],
188 ) -> Result<TransportResponse<'buffer>, Self::Error> {
189 self.send_inner(request, response_body)
190 }
191}
192
193impl AsyncTransport for MockTransport<'_> {
194 type Error = MockError;
195
196 async fn send<'transport, 'request, 'buffer>(
197 &'transport mut self,
198 request: TransportRequest<'request>,
199 response_body: &'buffer mut [u8],
200 ) -> Result<TransportResponse<'buffer>, Self::Error>
201 where
202 'request: 'transport,
203 'buffer: 'transport,
204 {
205 self.send_inner(request, response_body)
206 }
207}
208
209impl fmt::Debug for MockTransport<'_> {
210 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211 formatter
212 .debug_struct("MockTransport")
213 .field("remaining", &self.remaining())
214 .finish_non_exhaustive()
215 }
216}