#![allow(dead_code)]
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use axum::Router;
use axum::body::Bytes;
use axum::extract::State;
use axum::http::{HeaderMap, Method, StatusCode, Uri, header};
use axum::response::{IntoResponse, Response};
use cloud_tasks_emulator::proto::google::cloud::tasks::v2::cloud_tasks_client::CloudTasksClient;
use cloud_tasks_emulator::proto::google::cloud::tasks::v2::{GetTaskRequest, Task, task};
use tokio::net::TcpListener;
use tonic::Status;
use tonic::transport::Channel;
pub async fn start_emulator() -> CloudTasksClient<Channel> {
start_emulator_with_url().await.0
}
pub async fn start_emulator_with_url() -> (CloudTasksClient<Channel>, String) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
tokio::spawn(cloud_tasks_emulator::serve(
listener,
url.clone(),
Vec::new(),
));
let client = CloudTasksClient::connect(url.clone()).await.unwrap();
(client, url)
}
#[derive(Clone)]
pub struct Receiver {
pub url: String,
requests: Arc<Mutex<Vec<Received>>>,
}
#[derive(Clone)]
pub struct Received {
pub path: String,
pub query: Option<String>,
pub time: SystemTime,
pub method: Method,
pub headers: HeaderMap,
pub body: Bytes,
}
impl Receiver {
pub async fn start() -> Self {
let requests = Arc::new(Mutex::new(Vec::new()));
let app = Router::new().fallback(respond).with_state(requests.clone());
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
Self { url, requests }
}
pub fn url_for(&self, key: &str, steps: &str) -> String {
format!("{}/{key}/{steps}", self.url)
}
pub fn received(&self, key: &str) -> Vec<Received> {
let prefix = format!("/{key}/");
self.requests
.lock()
.unwrap()
.iter()
.filter(|request| request.path.starts_with(&prefix))
.cloned()
.collect()
}
pub async fn wait_for(&self, key: &str, count: usize) -> Vec<Received> {
tokio::time::timeout(Duration::from_secs(120), async {
loop {
let requests = self.received(key);
if requests.len() >= count {
return requests;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.unwrap_or_else(|_| panic!("{count} requests to {key} in time"))
}
}
async fn respond(
State(requests): State<Arc<Mutex<Vec<Received>>>>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Bytes,
) -> Response {
let path = uri.path().to_owned();
let attempt = {
let mut requests = requests.lock().unwrap();
let attempt = requests
.iter()
.filter(|request| request.path == path)
.count();
requests.push(Received {
path: path.clone(),
query: uri.query().map(str::to_owned),
time: SystemTime::now(),
method,
headers,
body,
});
attempt
};
let (key, steps) = path.rsplit_once('/').unwrap();
let steps: Vec<&str> = steps.split(',').collect();
match steps[attempt.min(steps.len() - 1)] {
"slow" => {
tokio::time::sleep(Duration::from_secs(20)).await;
StatusCode::OK.into_response()
}
"redirect" => (
StatusCode::FOUND,
[(header::LOCATION, format!("{key}/200"))],
)
.into_response(),
status => StatusCode::from_u16(status.parse().unwrap())
.unwrap()
.into_response(),
}
}
pub async fn poll_task(
client: &mut CloudTasksClient<Channel>,
name: &str,
done: impl Fn(&Task) -> bool,
) -> Result<Task, Status> {
tokio::time::timeout(Duration::from_secs(5), async {
loop {
let request = GetTaskRequest {
name: name.to_owned(),
response_view: task::View::Full.into(),
};
let task = client.get_task(request).await?.into_inner();
if done(&task) {
return Ok(task);
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("task reached the expected state in time")
}