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