Skip to main content

cloud_sdk_testkit/
mock.rs

1//! Deterministic no-allocation mock transport.
2
3use 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/// Expected request fields for one mock exchange.
13#[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    /// Creates a bodyless expected request.
22    #[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    /// Adds the exact expected request body.
32    #[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/// One expected request and deterministic response.
63#[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    /// Creates one mock exchange.
71    #[must_use]
72    pub const fn new(request: ExpectedRequest<'a>, response: ResponseFixture<'a>) -> Self {
73        Self { request, response }
74    }
75}
76
77/// Deterministic mock transport failure.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum MockError {
80    /// No expected exchange remains.
81    Exhausted,
82    /// HTTP method differs from the next expectation.
83    MethodMismatch,
84    /// Request target differs from the next expectation.
85    TargetMismatch,
86    /// Request body differs from the next expectation.
87    BodyMismatch,
88    /// Caller response buffer cannot hold the complete fixture body.
89    ResponseBufferTooSmall,
90    /// Internal cursor arithmetic failed closed.
91    CursorOverflow,
92    /// Fixture metadata could not be represented by the core transport.
93    InvalidFixtureMetadata,
94}
95
96/// Ordered no-allocation mock implementation of [`BlockingTransport`].
97pub struct MockTransport<'a> {
98    exchanges: &'a [MockExchange<'a>],
99    cursor: usize,
100}
101
102impl<'a> MockTransport<'a> {
103    /// Creates a mock over an ordered exchange slice.
104    #[must_use]
105    pub const fn new(exchanges: &'a [MockExchange<'a>]) -> Self {
106        Self {
107            exchanges,
108            cursor: 0,
109        }
110    }
111
112    /// Returns the number of exchanges not yet consumed.
113    #[must_use]
114    pub const fn remaining(&self) -> usize {
115        self.exchanges.len().saturating_sub(self.cursor)
116    }
117
118    /// Reports whether every expected exchange was consumed.
119    #[must_use]
120    pub const fn is_complete(&self) -> bool {
121        self.remaining() == 0
122    }
123
124    fn send_inner<'buffer>(
125        &mut self,
126        request: TransportRequest<'_>,
127        response_body: &'buffer mut [u8],
128    ) -> Result<TransportResponse<'buffer>, MockError> {
129        let exchange = self
130            .exchanges
131            .get(self.cursor)
132            .ok_or(MockError::Exhausted)?;
133        if request.method() != exchange.request.method() {
134            return Err(MockError::MethodMismatch);
135        }
136        if request.target() != exchange.request.target() {
137            return Err(MockError::TargetMismatch);
138        }
139        if request.body() != exchange.request.body() {
140            return Err(MockError::BodyMismatch);
141        }
142        let next_cursor = self
143            .cursor
144            .checked_add(1)
145            .ok_or(MockError::CursorOverflow)?;
146        let body_len =
147            exchange
148                .response
149                .body()
150                .write_to(response_body)
151                .map_err(|error| match error {
152                    FixtureBodyError::OutputTooSmall | FixtureBodyError::TooLarge => {
153                        MockError::ResponseBufferTooSmall
154                    }
155                })?;
156        let initialized = response_body
157            .get(..body_len)
158            .ok_or(MockError::ResponseBufferTooSmall)?;
159        self.cursor = next_cursor;
160        let response = TransportResponse::new(exchange.response.status(), initialized);
161        let rate_limit = exchange
162            .response
163            .rate_limit()
164            .map(|value| value.into_rate_limit())
165            .transpose()
166            .map_err(|_| MockError::InvalidFixtureMetadata)?;
167        Ok(rate_limit.map_or(response, |value| response.with_rate_limit(value)))
168    }
169}
170
171impl BlockingTransport for MockTransport<'_> {
172    type Error = MockError;
173
174    fn send<'buffer>(
175        &mut self,
176        request: TransportRequest<'_>,
177        response_body: &'buffer mut [u8],
178    ) -> Result<TransportResponse<'buffer>, Self::Error> {
179        self.send_inner(request, response_body)
180    }
181}
182
183impl AsyncTransport for MockTransport<'_> {
184    type Error = MockError;
185
186    async fn send<'transport, 'request, 'buffer>(
187        &'transport mut self,
188        request: TransportRequest<'request>,
189        response_body: &'buffer mut [u8],
190    ) -> Result<TransportResponse<'buffer>, Self::Error>
191    where
192        'request: 'transport,
193        'buffer: 'transport,
194    {
195        self.send_inner(request, response_body)
196    }
197}
198
199impl fmt::Debug for MockTransport<'_> {
200    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
201        formatter
202            .debug_struct("MockTransport")
203            .field("remaining", &self.remaining())
204            .finish_non_exhaustive()
205    }
206}