1use core::fmt;
4use core::sync::atomic::{AtomicUsize, Ordering};
5
6use cloud_sdk::Method;
7use cloud_sdk::transport::{
8 AsyncTransport, BlockingTransport, BoundTransport, ContentType, EndpointIdentity,
9 EndpointIdentityError, RequestTarget, ResponseContentType, ResponseStorageSanitizer,
10 TransportRequest, TransportResponse,
11};
12
13use crate::{FixtureBodyError, ResponseFixture};
14
15#[derive(Clone, Copy, Eq, PartialEq)]
17pub struct ExpectedRequest<'a> {
18 method: Method,
19 target: RequestTarget<'a>,
20 body: &'a [u8],
21 content_type: Option<ContentType<'a>>,
22}
23
24impl<'a> ExpectedRequest<'a> {
25 #[must_use]
27 pub const fn new(method: Method, target: RequestTarget<'a>) -> Self {
28 Self {
29 method,
30 target,
31 body: &[],
32 content_type: None,
33 }
34 }
35
36 #[must_use]
38 pub const fn with_body(mut self, body: &'a [u8]) -> Self {
39 self.body = body;
40 self
41 }
42
43 #[must_use]
45 pub const fn with_content_type(mut self, content_type: ContentType<'a>) -> Self {
46 self.content_type = Some(content_type);
47 self
48 }
49
50 const fn method(self) -> Method {
51 self.method
52 }
53
54 const fn target(self) -> RequestTarget<'a> {
55 self.target
56 }
57
58 const fn body(self) -> &'a [u8] {
59 self.body
60 }
61
62 const fn content_type(self) -> Option<ContentType<'a>> {
63 self.content_type
64 }
65}
66
67impl fmt::Debug for ExpectedRequest<'_> {
68 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69 formatter
70 .debug_struct("ExpectedRequest")
71 .field("method", &self.method)
72 .field("target", &"[redacted]")
73 .field("body", &"[redacted]")
74 .field("content_type", &self.content_type)
75 .finish()
76 }
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub struct MockExchange<'a> {
82 request: ExpectedRequest<'a>,
83 response: ResponseFixture<'a>,
84}
85
86impl<'a> MockExchange<'a> {
87 #[must_use]
89 pub const fn new(request: ExpectedRequest<'a>, response: ResponseFixture<'a>) -> Self {
90 Self { request, response }
91 }
92}
93
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum MockError {
97 Exhausted,
99 MethodMismatch,
101 TargetMismatch,
103 BodyMismatch,
105 ContentTypeMismatch,
107 ResponseBufferTooSmall,
109 CursorOverflow,
111 ConcurrentRequest,
113 InvalidFixtureMetadata,
115}
116
117impl_static_error!(MockError,
118 Self::Exhausted => "mock transport has no expected exchange remaining",
119 Self::MethodMismatch => "mock request method differs from expectation",
120 Self::TargetMismatch => "mock request target differs from expectation",
121 Self::BodyMismatch => "mock request body differs from expectation",
122 Self::ContentTypeMismatch => "mock request content type differs from expectation",
123 Self::ResponseBufferTooSmall => "mock response buffer is too small",
124 Self::CursorOverflow => "mock transport cursor overflowed",
125 Self::ConcurrentRequest => "mock transport cursor changed concurrently",
126 Self::InvalidFixtureMetadata => "mock fixture metadata is invalid",
127);
128
129pub struct MockTransport<'a> {
131 exchanges: &'a [MockExchange<'a>],
132 cursor: AtomicUsize,
133 endpoint: Option<EndpointIdentity<'a>>,
134}
135
136impl<'a> MockTransport<'a> {
137 #[must_use]
139 pub const fn new(exchanges: &'a [MockExchange<'a>]) -> Self {
140 Self {
141 exchanges,
142 cursor: AtomicUsize::new(0),
143 endpoint: None,
144 }
145 }
146
147 #[must_use]
149 pub const fn with_endpoint(mut self, endpoint: EndpointIdentity<'a>) -> Self {
150 self.endpoint = Some(endpoint);
151 self
152 }
153
154 #[must_use]
156 pub fn remaining(&self) -> usize {
157 self.exchanges
158 .len()
159 .saturating_sub(self.cursor.load(Ordering::Acquire))
160 }
161
162 #[must_use]
164 pub fn is_complete(&self) -> bool {
165 self.remaining() == 0
166 }
167
168 fn send_inner<'buffer>(
169 &self,
170 request: TransportRequest<'_>,
171 response_body: &'buffer mut [u8],
172 ) -> Result<TransportResponse<'buffer>, MockError> {
173 let cursor = self.cursor.load(Ordering::Acquire);
174 let exchange = self.exchanges.get(cursor).ok_or(MockError::Exhausted)?;
175 if request.method() != exchange.request.method() {
176 return Err(MockError::MethodMismatch);
177 }
178 if request.target() != exchange.request.target() {
179 return Err(MockError::TargetMismatch);
180 }
181 if request.body() != exchange.request.body() {
182 return Err(MockError::BodyMismatch);
183 }
184 if request.content_type() != exchange.request.content_type() {
185 return Err(MockError::ContentTypeMismatch);
186 }
187 let next_cursor = cursor.checked_add(1).ok_or(MockError::CursorOverflow)?;
188 let content_type = exchange
189 .response
190 .content_type()
191 .map(ResponseContentType::new)
192 .transpose()
193 .map_err(|_| MockError::InvalidFixtureMetadata)?;
194 let rate_limit = exchange
195 .response
196 .rate_limit()
197 .map(|value| value.into_rate_limit())
198 .transpose()
199 .map_err(|_| MockError::InvalidFixtureMetadata)?;
200 let body_len =
201 exchange
202 .response
203 .body()
204 .write_to(response_body)
205 .map_err(|error| match error {
206 FixtureBodyError::OutputTooSmall | FixtureBodyError::TooLarge => {
207 MockError::ResponseBufferTooSmall
208 }
209 })?;
210 let initialized = response_body
211 .get(..body_len)
212 .ok_or(MockError::ResponseBufferTooSmall)?;
213 let response = TransportResponse::new(exchange.response.status(), initialized);
214 let response = content_type.map_or(response, |value| response.with_content_type(value));
215 let response = rate_limit.map_or(response, |value| response.with_rate_limit(value));
216 self.cursor
217 .compare_exchange(cursor, next_cursor, Ordering::AcqRel, Ordering::Acquire)
218 .map_err(|_| MockError::ConcurrentRequest)?;
219 Ok(response)
220 }
221}
222
223impl BlockingTransport for MockTransport<'_> {
224 type Error = MockError;
225
226 fn send<'buffer>(
227 &self,
228 request: TransportRequest<'_>,
229 response_body: &'buffer mut [u8],
230 ) -> Result<TransportResponse<'buffer>, Self::Error> {
231 self.send_inner(request, response_body)
232 }
233}
234
235impl AsyncTransport for MockTransport<'_> {
236 type Error = MockError;
237
238 async fn send<'transport, 'request, 'buffer>(
239 &'transport self,
240 request: TransportRequest<'request>,
241 response_body: &'buffer mut [u8],
242 ) -> Result<TransportResponse<'buffer>, Self::Error>
243 where
244 'request: 'transport,
245 'buffer: 'transport,
246 {
247 self.send_inner(request, response_body)
248 }
249}
250
251impl ResponseStorageSanitizer for MockTransport<'_> {
252 fn sanitize_response_storage(&self, response_storage: &mut [u8]) {
253 response_storage.fill(0);
254 }
255}
256
257impl BoundTransport for MockTransport<'_> {
258 fn endpoint_identity(&self) -> Result<EndpointIdentity<'_>, EndpointIdentityError> {
259 self.endpoint.ok_or(EndpointIdentityError::UnboundTransport)
260 }
261}
262
263impl fmt::Debug for MockTransport<'_> {
264 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
265 formatter
266 .debug_struct("MockTransport")
267 .field("remaining", &self.remaining())
268 .finish_non_exhaustive()
269 }
270}