cratefield_oauth_client/
refresh.rs1use bytes::Bytes;
6use cratefield_core::{HttpClient, HttpError};
7use http::Request;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
15pub enum SendWithRefreshError<E> {
16 #[error("transport: {0}")]
18 Transport(#[from] HttpError),
19 #[error("the access token was rejected and the refresh failed")]
22 Refresh(E),
23}
24
25pub async fn send_with_refresh<E, R>(
42 http: &dyn HttpClient,
43 access_token: &str,
44 build: impl Fn(&str) -> Request<Bytes>,
45 refresh: impl FnOnce() -> R,
46) -> Result<http::Response<Bytes>, SendWithRefreshError<E>>
47where
48 R: Future<Output = Result<String, E>>,
49{
50 let response = http.send(build(access_token)).await?;
51 if response.status() != http::StatusCode::UNAUTHORIZED {
52 return Ok(response);
53 }
54 let fresh = refresh().await.map_err(SendWithRefreshError::Refresh)?;
55 Ok(http.send(build(&fresh)).await?)
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use async_trait::async_trait;
62 use std::sync::Arc;
63 use std::sync::atomic::{AtomicUsize, Ordering};
64
65 fn refreshing(
67 count: Arc<AtomicUsize>,
68 ) -> impl FnOnce() -> std::future::Ready<Result<String, ()>> {
69 move || {
70 count.fetch_add(1, Ordering::Relaxed);
71 std::future::ready(Ok("fresh-token".to_owned()))
72 }
73 }
74
75 async fn refusing() -> Result<String, ()> {
77 Err(())
78 }
79
80 struct Scripted {
82 statuses: Vec<u16>,
83 next: AtomicUsize,
84 sends: AtomicUsize,
85 }
86
87 impl Scripted {
88 fn with(statuses: &[u16]) -> Self {
89 Self {
90 statuses: statuses.to_vec(),
91 next: AtomicUsize::new(0),
92 sends: AtomicUsize::new(0),
93 }
94 }
95
96 fn sends(&self) -> usize {
97 self.sends.load(Ordering::Relaxed)
98 }
99 }
100
101 #[async_trait]
102 impl HttpClient for Scripted {
103 async fn send(&self, _request: Request<Bytes>) -> Result<http::Response<Bytes>, HttpError> {
104 self.sends.fetch_add(1, Ordering::Relaxed);
105 let index = self.next.fetch_add(1, Ordering::Relaxed);
106 let status = self.statuses.get(index).copied().unwrap_or(500);
107 Ok(http::Response::builder()
108 .status(status)
109 .body(Bytes::new())
110 .expect("status"))
111 }
112 }
113
114 fn request_for(token: &str) -> Request<Bytes> {
115 Request::builder()
116 .uri("https://provider.example/things")
117 .header("authorization", format!("Bearer {token}"))
118 .body(Bytes::new())
119 .expect("builds")
120 }
121
122 #[pollster::test]
123 async fn a_401_gets_one_refresh_and_one_replay() {
124 let http = Scripted::with(&[401, 201]);
125 let refreshed = Arc::new(AtomicUsize::new(0));
126 let response = send_with_refresh(
127 &http,
128 "stale-token",
129 request_for,
130 refreshing(Arc::clone(&refreshed)),
131 )
132 .await
133 .expect("the replay succeeds");
134 assert_eq!(response.status(), 201);
135 assert_eq!(refreshed.load(Ordering::Relaxed), 1, "exactly one refresh");
136 assert_eq!(http.sends(), 2, "one send, one replay");
137 }
138
139 #[pollster::test]
140 async fn a_failed_refresh_ends_the_sequence_without_a_replay() {
141 let http = Scripted::with(&[401]);
142 let error = send_with_refresh(&http, "stale-token", request_for, refusing)
143 .await
144 .expect_err("the refresh failed");
145 assert!(
146 matches!(error, SendWithRefreshError::Refresh(())),
147 "{error:?}"
148 );
149 assert_eq!(http.sends(), 1, "no replay after a failed refresh");
150 }
151
152 #[pollster::test]
153 async fn a_401_on_the_replay_is_data_not_a_loop() {
154 let http = Scripted::with(&[401, 401]);
155 let refreshed = Arc::new(AtomicUsize::new(0));
156 let response = send_with_refresh(
157 &http,
158 "stale-token",
159 request_for,
160 refreshing(Arc::clone(&refreshed)),
161 )
162 .await
163 .expect("the replay's status is returned, however unhelpful");
164 assert_eq!(response.status(), 401);
165 assert_eq!(
166 refreshed.load(Ordering::Relaxed),
167 1,
168 "never a second refresh"
169 );
170 assert_eq!(http.sends(), 2, "never a third send");
171 }
172
173 #[pollster::test]
174 async fn a_non_401_never_touches_the_refresh() {
175 let http = Scripted::with(&[403]);
176 let response = send_with_refresh(&http, "token", request_for, refusing)
177 .await
178 .expect("the status is passed through");
179 assert_eq!(response.status(), 403);
180 assert_eq!(http.sends(), 1);
181 }
182}