Skip to main content

better_fetch/backend/
recording.rs

1//! [`RecordingBackend`] — records the last [`HttpRequest`] and call counts for tests.
2
3use std::sync::atomic::{AtomicU32, Ordering};
4use std::sync::{Arc, Mutex};
5
6use async_trait::async_trait;
7use bytes::Bytes;
8use http::Method;
9
10use super::{HttpBackend, HttpBody, HttpRequest, HttpResponse, HttpStreamingResponse};
11use crate::Result;
12
13/// Kind of request body last observed (streaming bodies are not replayed).
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum RecordedBodyKind {
16    /// No body.
17    Empty,
18    /// Buffered byte body.
19    Bytes(Bytes),
20    /// Streaming body (not stored; only metadata).
21    Stream,
22}
23
24/// Snapshot of a recorded HTTP request for assertions in tests.
25#[derive(Debug, Clone)]
26pub struct RecordedRequest {
27    /// HTTP method.
28    pub method: Method,
29    /// Resolved URL.
30    pub url: url::Url,
31    /// Body kind (and bytes when buffered).
32    pub body: RecordedBodyKind,
33}
34
35/// Wraps an [`HttpBackend`] and records each executed request.
36#[derive(Clone)]
37pub struct RecordingBackend {
38    inner: Arc<dyn HttpBackend>,
39    state: Arc<RecordingState>,
40}
41
42#[derive(Default)]
43struct RecordingState {
44    last_recorded: Mutex<Option<RecordedRequest>>,
45    execute_count: AtomicU32,
46    execute_stream_count: AtomicU32,
47}
48
49fn snapshot_request(request: &HttpRequest) -> RecordedRequest {
50    let body = match &request.body {
51        HttpBody::Empty => RecordedBodyKind::Empty,
52        HttpBody::Bytes(bytes) => RecordedBodyKind::Bytes(bytes.clone()),
53        HttpBody::Stream(_) => RecordedBodyKind::Stream,
54    };
55    RecordedRequest {
56        method: request.method.clone(),
57        url: request.url.clone(),
58        body,
59    }
60}
61
62impl RecordingBackend {
63    /// Wraps `inner` and starts with empty recording state.
64    pub fn new(inner: Arc<dyn HttpBackend>) -> Self {
65        Self {
66            inner,
67            state: Arc::new(RecordingState::default()),
68        }
69    }
70
71    /// Returns the most recent recorded request snapshot.
72    pub fn last_recorded(&self) -> Option<RecordedRequest> {
73        self.state.last_recorded.lock().ok()?.clone()
74    }
75
76    /// Returns a clone of the most recent [`HttpRequest`] passed to the backend.
77    ///
78    /// **Note:** [`HttpBody::Stream`] is cloned as [`HttpBody::Empty`] — use [`Self::last_recorded`]
79    /// to assert streaming uploads.
80    pub fn last_request(&self) -> Option<HttpRequest> {
81        self.last_recorded().map(|recorded| HttpRequest {
82            method: recorded.method,
83            url: recorded.url,
84            body: match recorded.body {
85                RecordedBodyKind::Empty => HttpBody::Empty,
86                RecordedBodyKind::Bytes(bytes) => HttpBody::Bytes(bytes),
87                RecordedBodyKind::Stream => HttpBody::Empty,
88            },
89            headers: Default::default(),
90            timeout: None,
91            cancellation: None,
92            #[cfg(feature = "multipart")]
93            multipart: None,
94        })
95    }
96
97    /// Removes and returns the last recorded snapshot.
98    pub fn take_last_recorded(&self) -> Option<RecordedRequest> {
99        self.state.last_recorded.lock().ok()?.take()
100    }
101
102    /// Number of [`HttpBackend::execute`] calls.
103    pub fn execute_count(&self) -> u32 {
104        self.state.execute_count.load(Ordering::SeqCst)
105    }
106
107    /// Number of [`HttpBackend::execute_stream`] calls.
108    pub fn execute_stream_count(&self) -> u32 {
109        self.state.execute_stream_count.load(Ordering::SeqCst)
110    }
111
112    /// Total transport calls (`execute` + `execute_stream`).
113    pub fn total_calls(&self) -> u32 {
114        self.execute_count() + self.execute_stream_count()
115    }
116
117    fn record(&self, request: &HttpRequest) {
118        if let Ok(mut slot) = self.state.last_recorded.lock() {
119            *slot = Some(snapshot_request(request));
120        }
121    }
122}
123
124#[async_trait]
125impl HttpBackend for RecordingBackend {
126    async fn execute(&self, request: HttpRequest) -> Result<HttpResponse> {
127        self.state.execute_count.fetch_add(1, Ordering::SeqCst);
128        self.record(&request);
129        self.inner.execute(request).await
130    }
131
132    async fn execute_stream(&self, request: HttpRequest) -> Result<HttpStreamingResponse> {
133        self.state
134            .execute_stream_count
135            .fetch_add(1, Ordering::SeqCst);
136        self.record(&request);
137        self.inner.execute_stream(request).await
138    }
139}