use bytes::Bytes;
use cratefield_core::{HttpClient, HttpError};
use http::Request;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SendWithRefreshError<E> {
#[error("transport: {0}")]
Transport(#[from] HttpError),
#[error("the access token was rejected and the refresh failed")]
Refresh(E),
}
pub async fn send_with_refresh<E, R>(
http: &dyn HttpClient,
access_token: &str,
build: impl Fn(&str) -> Request<Bytes>,
refresh: impl FnOnce() -> R,
) -> Result<http::Response<Bytes>, SendWithRefreshError<E>>
where
R: Future<Output = Result<String, E>>,
{
let response = http.send(build(access_token)).await?;
if response.status() != http::StatusCode::UNAUTHORIZED {
return Ok(response);
}
let fresh = refresh().await.map_err(SendWithRefreshError::Refresh)?;
Ok(http.send(build(&fresh)).await?)
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
fn refreshing(
count: Arc<AtomicUsize>,
) -> impl FnOnce() -> std::future::Ready<Result<String, ()>> {
move || {
count.fetch_add(1, Ordering::Relaxed);
std::future::ready(Ok("fresh-token".to_owned()))
}
}
async fn refusing() -> Result<String, ()> {
Err(())
}
struct Scripted {
statuses: Vec<u16>,
next: AtomicUsize,
sends: AtomicUsize,
}
impl Scripted {
fn with(statuses: &[u16]) -> Self {
Self {
statuses: statuses.to_vec(),
next: AtomicUsize::new(0),
sends: AtomicUsize::new(0),
}
}
fn sends(&self) -> usize {
self.sends.load(Ordering::Relaxed)
}
}
#[async_trait]
impl HttpClient for Scripted {
async fn send(&self, _request: Request<Bytes>) -> Result<http::Response<Bytes>, HttpError> {
self.sends.fetch_add(1, Ordering::Relaxed);
let index = self.next.fetch_add(1, Ordering::Relaxed);
let status = self.statuses.get(index).copied().unwrap_or(500);
Ok(http::Response::builder()
.status(status)
.body(Bytes::new())
.expect("status"))
}
}
fn request_for(token: &str) -> Request<Bytes> {
Request::builder()
.uri("https://provider.example/things")
.header("authorization", format!("Bearer {token}"))
.body(Bytes::new())
.expect("builds")
}
#[pollster::test]
async fn a_401_gets_one_refresh_and_one_replay() {
let http = Scripted::with(&[401, 201]);
let refreshed = Arc::new(AtomicUsize::new(0));
let response = send_with_refresh(
&http,
"stale-token",
request_for,
refreshing(Arc::clone(&refreshed)),
)
.await
.expect("the replay succeeds");
assert_eq!(response.status(), 201);
assert_eq!(refreshed.load(Ordering::Relaxed), 1, "exactly one refresh");
assert_eq!(http.sends(), 2, "one send, one replay");
}
#[pollster::test]
async fn a_failed_refresh_ends_the_sequence_without_a_replay() {
let http = Scripted::with(&[401]);
let error = send_with_refresh(&http, "stale-token", request_for, refusing)
.await
.expect_err("the refresh failed");
assert!(
matches!(error, SendWithRefreshError::Refresh(())),
"{error:?}"
);
assert_eq!(http.sends(), 1, "no replay after a failed refresh");
}
#[pollster::test]
async fn a_401_on_the_replay_is_data_not_a_loop() {
let http = Scripted::with(&[401, 401]);
let refreshed = Arc::new(AtomicUsize::new(0));
let response = send_with_refresh(
&http,
"stale-token",
request_for,
refreshing(Arc::clone(&refreshed)),
)
.await
.expect("the replay's status is returned, however unhelpful");
assert_eq!(response.status(), 401);
assert_eq!(
refreshed.load(Ordering::Relaxed),
1,
"never a second refresh"
);
assert_eq!(http.sends(), 2, "never a third send");
}
#[pollster::test]
async fn a_non_401_never_touches_the_refresh() {
let http = Scripted::with(&[403]);
let response = send_with_refresh(&http, "token", request_for, refusing)
.await
.expect("the status is passed through");
assert_eq!(response.status(), 403);
assert_eq!(http.sends(), 1);
}
}