diff --git a/Cargo.toml b/Cargo.toml index f1cca5ee0..31f58a187 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,7 +118,6 @@ http-body-util = "0.1.0" rand = "0.8" mockito = "1.0.2" test-case = "3.0.0" -reqwest = { version = "0.12", features = ["blocking", "json"] } tower = { version = "0.5.1", features = ["util"] } aws-smithy-types = "1.0.1" aws-smithy-runtime = {version = "1.0.1", features = ["client", "test-util"]} diff --git a/src/build_queue.rs b/src/build_queue.rs index 530e65a44..4d18d03b5 100644 --- a/src/build_queue.rs +++ b/src/build_queue.rs @@ -472,11 +472,6 @@ impl BuildQueue { pub(crate) fn queued_crates(&self) -> Result> { self.runtime.block_on(self.inner.queued_crates()) } - #[cfg(test)] - pub(crate) fn has_build_queued(&self, name: &str, version: &str) -> Result { - self.runtime - .block_on(self.inner.has_build_queued(name, version)) - } } impl BuildQueue { diff --git a/src/docbuilder/rustwide_builder.rs b/src/docbuilder/rustwide_builder.rs index 868d24980..e1ba7ed77 100644 --- a/src/docbuilder/rustwide_builder.rs +++ b/src/docbuilder/rustwide_builder.rs @@ -1019,10 +1019,8 @@ impl Default for BuildPackageSummary { #[cfg(test)] mod tests { use super::*; - use crate::{ - db::types::Feature, - test::{assert_redirect, assert_success, wrapper, TestEnvironment}, - }; + use crate::db::types::Feature; + use crate::test::{wrapper, AxumRouterTestExt, TestEnvironment}; fn get_features( env: &TestEnvironment, @@ -1149,7 +1147,8 @@ mod tests { .collect(); targets.sort(); - let web = env.frontend(); + let runtime = env.runtime(); + let web = runtime.block_on(env.web_app()); // old rustdoc & source files are gone assert!(!storage.exists(&old_rustdoc_file)?); @@ -1169,12 +1168,13 @@ mod tests { None, &format!("{crate_path}/index.html"), )?); - assert_success(&format!("/{crate_}/{version}/{crate_path}"), web)?; + runtime.block_on(web.assert_success(&format!("/{crate_}/{version}/{crate_path}/")))?; // source is also packaged assert!(storage.exists_in_archive(&source_archive, None, "src/lib.rs",)?); - assert_success(&format!("/crate/{crate_}/{version}/source/src/lib.rs"), web)?; - + runtime.block_on( + web.assert_success(&format!("/crate/{crate_}/{version}/source/src/lib.rs")), + )?; assert!(!storage.exists_in_archive( &doc_archive, None, @@ -1183,11 +1183,10 @@ mod tests { let default_target_url = format!("/{crate_}/{version}/{default_target}/{crate_path}/index.html"); - assert_redirect( + runtime.block_on(web.assert_redirect( &default_target_url, &format!("/{crate_}/{version}/{crate_path}/index.html"), - web, - )?; + ))?; // Non-dist toolchains only have a single target, and of course // if include_default_targets is false we won't have this full list @@ -1219,7 +1218,7 @@ mod tests { format!("/{crate_}/{version}/{target}/{crate_path}/index.html"); assert!(target_docs_present); - assert_success(&target_url, web)?; + runtime.block_on(web.assert_success(&target_url))?; assert!(storage .exists(&format!("build-logs/{}/{target}.txt", row.build_id)) @@ -1355,12 +1354,14 @@ mod tests { None, &format!("{target}/{crate_path}/index.html"), )?; + assert!(target_docs_present); - let web = env.frontend(); - let target_url = format!("/{crate_}/{version}/{target}/{crate_path}/index.html"); + env.runtime().block_on(async { + let web = env.web_app().await; + let target_url = format!("/{crate_}/{version}/{target}/{crate_path}/index.html"); - assert!(target_docs_present); - assert_success(&target_url, web)?; + web.assert_success(&target_url).await + })?; Ok(()) }); diff --git a/src/test/mod.rs b/src/test/mod.rs index 89b7d4854..937366a8b 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -12,24 +12,18 @@ use crate::{ ServiceMetrics, }; use anyhow::Context as _; +use axum::body::Bytes; use axum::{async_trait, body::Body, http::Request, response::Response as AxumResponse, Router}; use fn_error_context::context; use futures_util::{stream::TryStreamExt, FutureExt}; use http_body_util::BodyExt; // for `collect` use once_cell::sync::OnceCell; -use reqwest::{ - blocking::{Client, ClientBuilder, RequestBuilder, Response}, - Method, -}; +use serde::de::DeserializeOwned; use sqlx::Connection as _; -use std::thread::{self, JoinHandle}; -use std::{ - fs, future::Future, net::SocketAddr, panic, rc::Rc, str::FromStr, sync::Arc, time::Duration, -}; +use std::{fs, future::Future, panic, rc::Rc, str::FromStr, sync::Arc}; use tokio::runtime::{Builder, Runtime}; -use tokio::sync::oneshot::Sender; use tower::ServiceExt; -use tracing::{debug, error, instrument, trace}; +use tracing::error; #[track_caller] pub(crate) fn wrapper(f: impl FnOnce(&TestEnvironment) -> Result<()>) { @@ -88,212 +82,152 @@ where } } -/// check a request if the cache control header matches NoCache -pub(crate) fn assert_no_cache(res: &Response) { - assert_eq!( - res.headers() - .get("Cache-Control") - .expect("missing cache-control header") - .to_str() - .unwrap(), - cache::NO_CACHING.to_str().unwrap(), - ); -} - -/// check a request if the cache control header matches the given cache config. -pub(crate) fn assert_cache_control( - res: &Response, - cache_policy: cache::CachePolicy, - config: &Config, -) { - assert!(config.cache_control_stale_while_revalidate.is_some()); - let cache_control = res.headers().get("Cache-Control"); - - if let Some(expected_directives) = cache_policy.render(config) { - assert_eq!( - cache_control - .expect("missing cache-control header") - .to_str() - .unwrap(), - expected_directives.to_str().unwrap(), - ); - } else { - assert!(cache_control.is_none()); - } -} - -/// Make sure that a URL returns a status code between 200-299 -pub(crate) fn assert_success(path: &str, web: &TestFrontend) -> Result<()> { - let status = web.get(path).send()?.status(); - assert!(status.is_success(), "failed to GET {path}: {status}"); - Ok(()) -} -/// Make sure that a URL returns a status code between 200-299, -/// also check the cache-control headers. -pub(crate) fn assert_success_cached( - path: &str, - web: &TestFrontend, - cache_policy: cache::CachePolicy, - config: &Config, -) -> Result<()> { - let response = web.get(path).send()?; - let status = response.status(); - assert!(status.is_success(), "failed to GET {path}: {status}"); - assert_cache_control(&response, cache_policy, config); - Ok(()) -} - -/// Make sure that a URL returns a 404 -pub(crate) fn assert_not_found(path: &str, web: &TestFrontend) -> Result<()> { - let response = web.get(path).send()?; - - // for now, 404s should always have `no-cache` - assert_no_cache(&response); - - assert_eq!(response.status(), 404, "GET {path} should have been a 404"); - Ok(()) -} - -fn assert_redirect_common( - path: &str, - expected_target: &str, - web: &TestFrontend, -) -> Result { - let response = web.get_no_redirect(path).send()?; - let status = response.status(); - if !status.is_redirection() { - anyhow::bail!("non-redirect from GET {path}: {status}"); - } - - let mut redirect_target = response - .headers() - .get("Location") - .context("missing 'Location' header")? - .to_str() - .context("non-ASCII redirect")?; - - if !expected_target.starts_with("http") { - // TODO: Should be able to use Url::make_relative, - // but https://github.com/servo/rust-url/issues/766 - let base = format!("http://{}", web.server_addr()); - redirect_target = redirect_target - .strip_prefix(&base) - .unwrap_or(redirect_target); - } - - if redirect_target != expected_target { - anyhow::bail!("got redirect to {redirect_target}"); - } - - Ok(response) -} - -/// Makes sure that a URL redirects to a specific page, but doesn't check that the target exists -/// -/// Returns the redirect response -#[context("expected redirect from {path} to {expected_target}")] -pub(crate) fn assert_redirect_unchecked( - path: &str, - expected_target: &str, - web: &TestFrontend, -) -> Result { - assert_redirect_common(path, expected_target, web) -} - -/// Makes sure that a URL redirects to a specific page, but doesn't check that the target exists -/// -/// Returns the redirect response -#[context("expected redirect from {path} to {expected_target}")] -pub(crate) fn assert_redirect_cached_unchecked( - path: &str, - expected_target: &str, - cache_policy: cache::CachePolicy, - web: &TestFrontend, - config: &Config, -) -> Result { - let redirect_response = assert_redirect_common(path, expected_target, web)?; - assert_cache_control(&redirect_response, cache_policy, config); - Ok(redirect_response) -} - -/// Make sure that a URL redirects to a specific page, and that the target exists and is not another redirect -/// -/// Returns the redirect response -#[context("expected redirect from {path} to {expected_target}")] -pub(crate) fn assert_redirect( - path: &str, - expected_target: &str, - web: &TestFrontend, -) -> Result { - let redirect_response = assert_redirect_common(path, expected_target, web)?; - - let response = web.get_no_redirect(expected_target).send()?; - let status = response.status(); - if !status.is_success() { - anyhow::bail!("failed to GET {expected_target}: {status}"); - } - - Ok(redirect_response) -} - -/// Make sure that a URL redirects to a specific page, and that the target exists and is not another redirect. -/// Also verifies that the redirect's cache-control header matches the provided cache policy. -/// -/// Returns the redirect response -#[context("expected redirect from {path} to {expected_target}")] -pub(crate) fn assert_redirect_cached( - path: &str, - expected_target: &str, - cache_policy: cache::CachePolicy, - web: &TestFrontend, - config: &Config, -) -> Result { - let redirect_response = assert_redirect_common(path, expected_target, web)?; - assert_cache_control(&redirect_response, cache_policy, config); - - let response = web.get_no_redirect(expected_target).send()?; - let status = response.status(); - if !status.is_success() { - anyhow::bail!("failed to GET {expected_target}: {status}"); - } - - Ok(redirect_response) -} - pub(crate) trait AxumResponseTestExt { - async fn text(self) -> String; + async fn text(self) -> Result; + async fn bytes(self) -> Result; + async fn json(self) -> Result; + fn redirect_target(&self) -> Option<&str>; + fn assert_cache_control(&self, cache_policy: cache::CachePolicy, config: &Config); + fn error_for_status(self) -> Result + where + Self: Sized; } impl AxumResponseTestExt for axum::response::Response { - async fn text(self) -> String { - String::from_utf8_lossy(&self.into_body().collect().await.unwrap().to_bytes()).to_string() + async fn text(self) -> Result { + Ok(String::from_utf8_lossy(&(self.bytes().await?)).to_string()) + } + async fn bytes(self) -> Result { + Ok(self.into_body().collect().await?.to_bytes()) + } + async fn json(self) -> Result { + let body = self.text().await?; + Ok(serde_json::from_str(&body)?) + } + fn redirect_target(&self) -> Option<&str> { + self.headers().get("Location")?.to_str().ok() + } + fn assert_cache_control(&self, cache_policy: cache::CachePolicy, config: &Config) { + assert!(config.cache_control_stale_while_revalidate.is_some()); + let cache_control = self.headers().get("Cache-Control"); + + if let Some(expected_directives) = cache_policy.render(config) { + assert_eq!( + cache_control + .expect("missing cache-control header") + .to_str() + .unwrap(), + expected_directives.to_str().unwrap(), + ); + } else { + assert!(cache_control.is_none()); + } + } + + fn error_for_status(self) -> Result + where + Self: Sized, + { + let status = self.status(); + if status.is_client_error() || status.is_server_error() { + anyhow::bail!("got status code {}", status); + } else { + Ok(self) + } } } pub(crate) trait AxumRouterTestExt { - async fn assert_success(&self, path: &str) -> Result<()>; + async fn get_and_follow_redirects(&self, path: &str) -> Result; + async fn assert_redirect_cached_unchecked( + &self, + path: &str, + expected_target: &str, + cache_policy: cache::CachePolicy, + config: &Config, + ) -> Result; + async fn assert_not_found(&self, path: &str) -> Result<()>; + async fn assert_success_cached( + &self, + path: &str, + cache_policy: cache::CachePolicy, + config: &Config, + ) -> Result<()>; + async fn assert_success(&self, path: &str) -> Result; async fn get(&self, path: &str) -> Result; + async fn post(&self, path: &str) -> Result; async fn assert_redirect_common( &self, path: &str, expected_target: &str, ) -> Result; async fn assert_redirect(&self, path: &str, expected_target: &str) -> Result; + async fn assert_redirect_unchecked( + &self, + path: &str, + expected_target: &str, + ) -> Result; + async fn assert_redirect_cached( + &self, + path: &str, + expected_target: &str, + cache_policy: cache::CachePolicy, + config: &Config, + ) -> Result; } impl AxumRouterTestExt for axum::Router { /// Make sure that a URL returns a status code between 200-299 - async fn assert_success(&self, path: &str) -> Result<()> { - let response = self - .clone() - .oneshot(Request::builder().uri(path).body(Body::empty()).unwrap()) - .await?; + async fn assert_success(&self, path: &str) -> Result { + let response = self.get(path).await?; let status = response.status(); + if status.is_redirection() { + panic!( + "expected success response from {path}, got redirect ({status}) to {:?}", + response.redirect_target() + ); + } assert!(status.is_success(), "failed to GET {path}: {status}"); + Ok(response) + } + + async fn assert_not_found(&self, path: &str) -> Result<()> { + let response = self.get(path).await?; + + // for now, 404s should always have `no-cache` + // assert_no_cache(&response); + assert_eq!( + response + .headers() + .get("Cache-Control") + .expect("missing cache-control header") + .to_str() + .unwrap(), + cache::NO_CACHING.to_str().unwrap(), + ); + + assert_eq!(response.status(), 404, "GET {path} should have been a 404"); Ok(()) } - /// simple `get` method + + async fn assert_success_cached( + &self, + path: &str, + cache_policy: cache::CachePolicy, + config: &Config, + ) -> Result<()> { + let response = self.get(path).await?; + let status = response.status(); + assert!( + status.is_success(), + "failed to GET {path}: {status} (redirect: {})", + response.redirect_target().unwrap_or_default() + ); + response.assert_cache_control(cache_policy, config); + Ok(()) + } + async fn get(&self, path: &str) -> Result { Ok(self .clone() @@ -301,6 +235,34 @@ impl AxumRouterTestExt for axum::Router { .await?) } + async fn get_and_follow_redirects(&self, path: &str) -> Result { + let mut path = path.to_owned(); + for _ in 0..=10 { + let response = self.get(&path).await?; + if response.status().is_redirection() { + if let Some(target) = response.redirect_target() { + path = target.to_owned(); + continue; + } + } + return Ok(response); + } + panic!("redirect loop"); + } + + async fn post(&self, path: &str) -> Result { + Ok(self + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri(path) + .body(Body::empty()) + .unwrap(), + ) + .await?) + } + async fn assert_redirect_common( &self, path: &str, @@ -313,11 +275,8 @@ impl AxumRouterTestExt for axum::Router { } let redirect_target = response - .headers() - .get("Location") - .context("missing 'Location' header")? - .to_str() - .context("non-ASCII redirect")?; + .redirect_target() + .context("missing 'Location' header")?; // FIXME: not sure we need this // if !expected_target.starts_with("http") { @@ -348,6 +307,45 @@ impl AxumRouterTestExt for axum::Router { Ok(redirect_response) } + + async fn assert_redirect_unchecked( + &self, + path: &str, + expected_target: &str, + ) -> Result { + self.assert_redirect_common(path, expected_target).await + } + + async fn assert_redirect_cached( + &self, + path: &str, + expected_target: &str, + cache_policy: cache::CachePolicy, + config: &Config, + ) -> Result { + let redirect_response = self.assert_redirect_common(path, expected_target).await?; + redirect_response.assert_cache_control(cache_policy, config); + + let response = self.get(expected_target).await?; + let status = response.status(); + if !status.is_success() { + anyhow::bail!("failed to GET {expected_target}: {status}"); + } + + Ok(redirect_response) + } + + async fn assert_redirect_cached_unchecked( + &self, + path: &str, + expected_target: &str, + cache_policy: cache::CachePolicy, + config: &Config, + ) -> Result { + let redirect_response = self.assert_redirect_common(path, expected_target).await?; + redirect_response.assert_cache_control(cache_policy, config); + Ok(redirect_response) + } } pub(crate) struct TestEnvironment { @@ -363,7 +361,6 @@ pub(crate) struct TestEnvironment { runtime: OnceCell>, instance_metrics: OnceCell>, service_metrics: OnceCell>, - frontend: OnceCell, repository_stats_updater: OnceCell>, } @@ -398,16 +395,12 @@ impl TestEnvironment { registry_api: OnceCell::new(), instance_metrics: OnceCell::new(), service_metrics: OnceCell::new(), - frontend: OnceCell::new(), runtime: OnceCell::new(), repository_stats_updater: OnceCell::new(), } } fn cleanup(self) { - if let Some(frontend) = self.frontend.into_inner() { - frontend.shutdown(); - } if let Some(storage) = self.storage.get() { storage .cleanup_after_test() @@ -428,7 +421,7 @@ impl TestEnvironment { fs::create_dir_all(config.registry_index_path.clone()).unwrap(); // Use less connections for each test compared to production. - config.max_pool_size = 4; + config.max_pool_size = 8; config.min_pool_idle = 0; // Use the database for storage, as it's faster than S3. @@ -608,19 +601,6 @@ impl TestEnvironment { .await } - pub(crate) fn override_frontend(&self, init: impl FnOnce(&mut TestFrontend)) -> &TestFrontend { - let mut frontend = TestFrontend::new(self); - init(&mut frontend); - if self.frontend.set(frontend).is_err() { - panic!("cannot call override_frontend after frontend is initialized"); - } - self.frontend.get().unwrap() - } - - pub(crate) fn frontend(&self) -> &TestFrontend { - self.frontend.get_or_init(|| TestFrontend::new(self)) - } - pub(crate) fn fake_release(&self) -> fakes::FakeRelease { self.runtime().block_on(self.async_fake_release()) } @@ -796,113 +776,3 @@ impl Drop for TestDatabase { }); } } - -pub(crate) struct TestFrontend { - axum_server_thread: JoinHandle<()>, - axum_server_shutdown_signal: Sender<()>, - axum_server_address: SocketAddr, - pub(crate) client: Client, - pub(crate) client_no_redirect: Client, -} - -impl TestFrontend { - #[instrument(skip_all)] - fn new(context: &dyn Context) -> Self { - fn build(f: impl FnOnce(ClientBuilder) -> ClientBuilder) -> Client { - let base = Client::builder() - .connect_timeout(Duration::from_millis(2000)) - .timeout(Duration::from_millis(2000)) - // The test server only supports a single connection, so having two clients with - // idle connections deadlocks the tests - .pool_max_idle_per_host(0); - f(base).build().unwrap() - } - - debug!("loading template data"); - let template_data = Arc::new(TemplateData::new(1).unwrap()); - - let runtime = context.runtime().unwrap(); - - debug!("binding local TCP port for axum"); - let axum_listener = runtime - .block_on(tokio::net::TcpListener::bind( - "127.0.0.1:0".parse::().unwrap(), - )) - .unwrap(); - - let axum_addr = axum_listener.local_addr().unwrap(); - debug!("bound to local address: {}", axum_addr); - - let (tx, rx) = tokio::sync::oneshot::channel::<()>(); - - debug!("building axum app"); - let runtime = context.runtime().unwrap(); - let axum_app = runtime - .block_on(build_axum_app(context, template_data)) - .expect("could not build axum app"); - - let handle = thread::spawn({ - move || { - runtime.block_on(async { - axum::serve(axum_listener, axum_app.into_make_service()) - .with_graceful_shutdown(async { - rx.await.ok(); - }) - .await - .expect("error from axum server") - }) - } - }); - - Self { - axum_server_address: axum_addr, - axum_server_thread: handle, - axum_server_shutdown_signal: tx, - client: build(|b| b), - client_no_redirect: build(|b| b.redirect(reqwest::redirect::Policy::none())), - } - } - - #[instrument(skip_all)] - fn shutdown(self) { - trace!("sending axum shutdown signal"); - self.axum_server_shutdown_signal - .send(()) - .expect("could not send shutdown signal"); - - trace!("joining axum server thread"); - self.axum_server_thread - .join() - .expect("could not join axum background thread"); - } - - fn build_url(&self, url: &str) -> String { - if url.is_empty() || url.starts_with('/') { - format!("http://{}{}", self.axum_server_address, url) - } else { - url.to_owned() - } - } - - pub(crate) fn server_addr(&self) -> SocketAddr { - self.axum_server_address - } - - pub(crate) fn get(&self, url: &str) -> RequestBuilder { - let url = self.build_url(url); - debug!("getting {url}"); - self.client.request(Method::GET, url) - } - - pub(crate) fn post(&self, url: &str) -> RequestBuilder { - let url = self.build_url(url); - debug!("posting {url}"); - self.client.request(Method::POST, url) - } - - pub(crate) fn get_no_redirect(&self, url: &str) -> RequestBuilder { - let url = self.build_url(url); - debug!("getting {url} (no redirects)"); - self.client_no_redirect.request(Method::GET, url) - } -} diff --git a/src/utils/html.rs b/src/utils/html.rs index 49243a01c..548c7310f 100644 --- a/src/utils/html.rs +++ b/src/utils/html.rs @@ -107,12 +107,12 @@ pub(crate) fn rewrite_lol( #[cfg(test)] mod test { - use crate::test::wrapper; + use crate::test::{async_wrapper, AxumResponseTestExt, AxumRouterTestExt}; #[test] fn rewriting_only_injects_css_once() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release().await .name("testing") .version("0.1.0") // A somewhat representative rustdoc html file from 2016 @@ -139,12 +139,13 @@ mod test { "#) - .create()?; + .create_async().await?; - let output = env.frontend().get("/testing/0.1.0/2016/").send()?.text()?; + let web = env.web_app().await; + let output = web.get("/testing/0.1.0/2016/").await?.text().await?; assert_eq!(output.matches(r#"href="/-/static/vendored.css"#).count(), 1); - let output = env.frontend().get("/testing/0.1.0/2022/").send()?.text()?; + let output = web.get("/testing/0.1.0/2022/").await?.text().await?; assert_eq!(output.matches(r#"href="/-/static/vendored.css"#).count(), 1); Ok(()) diff --git a/src/web/build_details.rs b/src/web/build_details.rs index 9888ebaf5..b7e481aed 100644 --- a/src/web/build_details.rs +++ b/src/web/build_details.rs @@ -146,7 +146,10 @@ pub(crate) async fn build_details_handler( #[cfg(test)] mod tests { - use crate::test::{fake_release_that_failed_before_build, wrapper, FakeBuild}; + use crate::test::{ + async_wrapper, fake_release_that_failed_before_build, AxumResponseTestExt, + AxumRouterTestExt, FakeBuild, + }; use kuchikiki::traits::TendrilSink; use test_case::test_case; @@ -165,24 +168,24 @@ mod tests { #[test] fn test_partial_build_result() { - wrapper(|env| { - let (_, build_id) = env.runtime().block_on(async { - let mut conn = env.async_db().await.async_conn().await; - fake_release_that_failed_before_build( - &mut conn, - "foo", - "0.1.0", - "some random error", - ) - .await - })?; + async_wrapper(|env| async move { + let mut conn = env.async_db().await.async_conn().await; + let (_, build_id) = fake_release_that_failed_before_build( + &mut conn, + "foo", + "0.1.0", + "some random error", + ) + .await?; let page = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get(&format!("/crate/foo/0.1.0/builds/{build_id}")) - .send()? + .await? .error_for_status()? - .text()?, + .text() + .await?, ); let info_text = page.select("pre").unwrap().next().unwrap().text_contents(); @@ -196,28 +199,34 @@ mod tests { #[test] fn db_build_logs() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo") .version("0.1.0") .builds(vec![FakeBuild::default() .no_s3_build_log() .db_build_log("A build log")]) - .create()?; + .create_async() + .await?; + + let web = env.web_app().await; let page = kuchikiki::parse_html().one( - env.frontend() - .get("/crate/foo/0.1.0/builds") - .send()? + web.get("/crate/foo/0.1.0/builds") + .await? .error_for_status()? - .text()?, + .text() + .await?, ); let node = page.select("ul > li a.release").unwrap().next().unwrap(); - let attrs = node.attributes.borrow(); - let url = attrs.get("href").unwrap(); + let url = { + let attrs = node.attributes.borrow(); + attrs.get("href").unwrap().to_owned() + }; - let page = kuchikiki::parse_html().one(env.frontend().get(url).send()?.text()?); + let page = kuchikiki::parse_html().one(web.get(&url).await?.text().await?); assert!(get_all_log_links(&page).is_empty()); let log = page.select("pre").unwrap().next().unwrap().text_contents(); @@ -230,25 +239,27 @@ mod tests { #[test] fn s3_build_logs() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo") .version("0.1.0") .builds(vec![FakeBuild::default().s3_build_log("A build log")]) - .create()?; + .create_async() + .await?; - let page = kuchikiki::parse_html().one( - env.frontend() - .get("/crate/foo/0.1.0/builds") - .send()? - .text()?, - ); + let web = env.web_app().await; + + let page = kuchikiki::parse_html() + .one(web.get("/crate/foo/0.1.0/builds").await?.text().await?); let node = page.select("ul > li a.release").unwrap().next().unwrap(); - let attrs = node.attributes.borrow(); - let build_url = attrs.get("href").unwrap(); + let build_url = { + let attrs = node.attributes.borrow(); + attrs.get("href").unwrap().to_owned() + }; - let page = kuchikiki::parse_html().one(env.frontend().get(build_url).send()?.text()?); + let page = kuchikiki::parse_html().one(web.get(&build_url).await?.text().await?); let log = page.select("pre").unwrap().next().unwrap().text_contents(); @@ -265,7 +276,7 @@ mod tests { // now get the log with the specific filename in the URL let log = kuchikiki::parse_html() - .one(env.frontend().get(&all_log_links[0].1).send()?.text()?) + .one(web.get(&all_log_links[0].1).await?.text().await?) .select("pre") .unwrap() .next() @@ -280,8 +291,9 @@ mod tests { #[test] fn s3_build_logs_multiple_targets() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo") .version("0.1.0") .builds(vec![FakeBuild::default() @@ -290,20 +302,21 @@ mod tests { "other_target", "other target build log", )]) - .create()?; + .create_async() + .await?; - let page = kuchikiki::parse_html().one( - env.frontend() - .get("/crate/foo/0.1.0/builds") - .send()? - .text()?, - ); + let web = env.web_app().await; + + let page = kuchikiki::parse_html() + .one(web.get("/crate/foo/0.1.0/builds").await?.text().await?); let node = page.select("ul > li a.release").unwrap().next().unwrap(); - let attrs = node.attributes.borrow(); - let build_url = attrs.get("href").unwrap(); + let build_url = { + let attrs = node.attributes.borrow(); + attrs.get("href").unwrap().to_owned() + }; - let page = kuchikiki::parse_html().one(env.frontend().get(build_url).send()?.text()?); + let page = kuchikiki::parse_html().one(web.get(&build_url).await?.text().await?); let log = page.select("pre").unwrap().next().unwrap().text_contents(); @@ -329,7 +342,7 @@ mod tests { (&all_log_links[1].1, "A build log"), ] { let other_log = kuchikiki::parse_html() - .one(env.frontend().get(url).send()?.text()?) + .one(web.get(url).await?.text().await?) .select("pre") .unwrap() .next() @@ -345,27 +358,29 @@ mod tests { #[test] fn both_build_logs() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo") .version("0.1.0") .builds(vec![FakeBuild::default() .s3_build_log("A build log") .db_build_log("Another build log")]) - .create()?; + .create_async() + .await?; - let page = kuchikiki::parse_html().one( - env.frontend() - .get("/crate/foo/0.1.0/builds") - .send()? - .text()?, - ); + let web = env.web_app().await; + + let page = kuchikiki::parse_html() + .one(web.get("/crate/foo/0.1.0/builds").await?.text().await?); let node = page.select("ul > li a.release").unwrap().next().unwrap(); - let attrs = node.attributes.borrow(); - let url = attrs.get("href").unwrap(); + let url = { + let attrs = node.attributes.borrow(); + attrs.get("href").unwrap().to_owned() + }; - let page = kuchikiki::parse_html().one(env.frontend().get(url).send()?.text()?); + let page = kuchikiki::parse_html().one(web.get(&url).await?.text().await?); let log = page.select("pre").unwrap().next().unwrap().text_contents(); @@ -379,15 +394,21 @@ mod tests { #[test_case("42")] #[test_case("nan")] fn non_existing_build(build_id: &str) { - wrapper(|env| { - env.fake_release().name("foo").version("0.1.0").create()?; + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("foo") + .version("0.1.0") + .create_async() + .await?; let res = env - .frontend() + .web_app() + .await .get(&format!("/crate/foo/0.1.0/builds/{build_id}")) - .send()?; + .await?; assert_eq!(res.status(), 404); - assert!(res.text()?.contains("no such build")); + assert!(res.text().await?.contains("no such build")); Ok(()) }); diff --git a/src/web/builds.rs b/src/web/builds.rs index 74cc5d132..01542c3b6 100644 --- a/src/web/builds.rs +++ b/src/web/builds.rs @@ -255,29 +255,32 @@ mod tests { use super::BuildStatus; use crate::{ db::Overrides, - test::{assert_cache_control, fake_release_that_failed_before_build, wrapper, FakeBuild}, + test::{ + async_wrapper, fake_release_that_failed_before_build, AxumResponseTestExt, + AxumRouterTestExt, FakeBuild, + }, web::cache::CachePolicy, }; + use axum::{body::Body, http::Request}; use chrono::{DateTime, Utc}; use kuchikiki::traits::TendrilSink; use reqwest::StatusCode; + use tower::ServiceExt; #[test] fn build_list_empty_build() { - wrapper(|env| { - env.runtime().block_on(async { - let mut conn = env.async_db().await.async_conn().await; - fake_release_that_failed_before_build(&mut conn, "foo", "0.1.0", "some errors") - .await - })?; + async_wrapper(|env| async move { + let mut conn = env.async_db().await.async_conn().await; + fake_release_that_failed_before_build(&mut conn, "foo", "0.1.0", "some errors").await?; let response = env - .frontend() + .web_app() + .await .get("/crate/foo/0.1.0/builds") - .send()? + .await? .error_for_status()?; - assert_cache_control(&response, CachePolicy::NoCaching, &env.config()); - let page = kuchikiki::parse_html().one(response.text()?); + response.assert_cache_control(CachePolicy::NoCaching, &env.config()); + let page = kuchikiki::parse_html().one(response.text().await?); let rows: Vec<_> = page .select("ul > li a.release") @@ -295,8 +298,9 @@ mod tests { #[test] fn build_list() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo") .version("0.1.0") .builds(vec![ @@ -315,11 +319,12 @@ mod tests { .rustc_version("rustc (blabla 2022-01-01)") .docsrs_version("docs.rs 4.0.0"), ]) - .create()?; + .create_async() + .await?; - let response = env.frontend().get("/crate/foo/0.1.0/builds").send()?; - assert_cache_control(&response, CachePolicy::NoCaching, &env.config()); - let page = kuchikiki::parse_html().one(response.text()?); + let response = env.web_app().await.get("/crate/foo/0.1.0/builds").await?; + response.assert_cache_control(CachePolicy::NoCaching, &env.config()); + let page = kuchikiki::parse_html().one(response.text().await?); let rows: Vec<_> = page .select("ul > li a.release") @@ -340,8 +345,9 @@ mod tests { #[test] fn build_list_json() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo") .version("0.1.0") .builds(vec![ @@ -360,11 +366,16 @@ mod tests { .rustc_version("rustc (blabla 2022-01-01)") .docsrs_version("docs.rs 4.0.0"), ]) - .create()?; + .create_async() + .await?; - let response = env.frontend().get("/crate/foo/0.1.0/builds.json").send()?; - assert_cache_control(&response, CachePolicy::NoStoreMustRevalidate, &env.config()); - let value: serde_json::Value = serde_json::from_str(&response.text()?)?; + let response = env + .web_app() + .await + .get("/crate/foo/0.1.0/builds.json") + .await?; + response.assert_cache_control(CachePolicy::NoStoreMustRevalidate, &env.config()); + let value: serde_json::Value = serde_json::from_str(&response.text().await?)?; assert_eq!(value.as_array().unwrap().len(), 3); @@ -428,20 +439,33 @@ mod tests { #[test] fn build_trigger_rebuild_missing_config() { - wrapper(|env| { + async_wrapper(|env| async move { env.override_config(|config| config.cratesio_token = None); - env.fake_release().name("foo").version("0.1.0").create()?; + env.async_fake_release() + .await + .name("foo") + .version("0.1.0") + .create_async() + .await?; { - let response = env.frontend().get("/crate/regex/1.3.1/rebuild").send()?; + let response = env + .web_app() + .await + .get("/crate/regex/1.3.1/rebuild") + .await?; // Needs POST assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); } { - let response = env.frontend().post("/crate/regex/1.3.1/rebuild").send()?; + let response = env + .web_app() + .await + .post("/crate/regex/1.3.1/rebuild") + .await?; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - let json: serde_json::Value = response.json()?; + let json: serde_json::Value = response.json().await?; assert_eq!( json, serde_json::json!({ @@ -457,16 +481,25 @@ mod tests { #[test] fn build_trigger_rebuild_with_config() { - wrapper(|env| { + async_wrapper(|env| async move { let correct_token = "foo137"; env.override_config(|config| config.cratesio_token = Some(correct_token.into())); - env.fake_release().name("foo").version("0.1.0").create()?; + env.async_fake_release() + .await + .name("foo") + .version("0.1.0") + .create_async() + .await?; { - let response = env.frontend().post("/crate/regex/1.3.1/rebuild").send()?; + let response = env + .web_app() + .await + .post("/crate/regex/1.3.1/rebuild") + .await?; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - let json: serde_json::Value = response.json()?; + let json: serde_json::Value = response.json().await?; assert_eq!( json, serde_json::json!({ @@ -477,13 +510,19 @@ mod tests { } { - let response = env - .frontend() - .post("/crate/regex/1.3.1/rebuild") - .bearer_auth("someinvalidtoken") - .send()?; + let app = env.web_app().await; + let response = app + .oneshot( + Request::builder() + .uri("/crate/regex/1.3.1/rebuild") + .method("POST") + .header("Authorization", "Bearer someinvalidtoken") + .body(Body::empty()) + .unwrap(), + ) + .await?; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - let json: serde_json::Value = response.json()?; + let json: serde_json::Value = response.json().await?; assert_eq!( json, serde_json::json!({ @@ -493,31 +532,45 @@ mod tests { ); } - assert_eq!(env.build_queue().pending_count()?, 0); - assert!(!env.build_queue().has_build_queued("foo", "0.1.0")?); + let build_queue = env.async_build_queue().await; + + assert_eq!(build_queue.pending_count().await?, 0); + assert!(!build_queue.has_build_queued("foo", "0.1.0").await?); { - let response = env - .frontend() - .post("/crate/foo/0.1.0/rebuild") - .bearer_auth(correct_token) - .send()?; + let app = env.web_app().await; + let response = app + .oneshot( + Request::builder() + .uri("/crate/foo/0.1.0/rebuild") + .method("POST") + .header("Authorization", &format!("Bearer {}", correct_token)) + .body(Body::empty()) + .unwrap(), + ) + .await?; assert_eq!(response.status(), StatusCode::CREATED); - let json: serde_json::Value = response.json()?; + let json: serde_json::Value = response.json().await?; assert_eq!(json, serde_json::json!({})); } - assert_eq!(env.build_queue().pending_count()?, 1); - assert!(env.build_queue().has_build_queued("foo", "0.1.0")?); + assert_eq!(build_queue.pending_count().await?, 1); + assert!(build_queue.has_build_queued("foo", "0.1.0").await?); { - let response = env - .frontend() - .post("/crate/foo/0.1.0/rebuild") - .bearer_auth(correct_token) - .send()?; + let app = env.web_app().await; + let response = app + .oneshot( + Request::builder() + .uri("/crate/foo/0.1.0/rebuild") + .method("POST") + .header("Authorization", &format!("Bearer {}", correct_token)) + .body(Body::empty()) + .unwrap(), + ) + .await?; assert_eq!(response.status(), StatusCode::BAD_REQUEST); - let json: serde_json::Value = response.json()?; + let json: serde_json::Value = response.json().await?; assert_eq!( json, serde_json::json!({ @@ -527,8 +580,8 @@ mod tests { ); } - assert_eq!(env.build_queue().pending_count()?, 1); - assert!(env.build_queue().has_build_queued("foo", "0.1.0")?); + assert_eq!(build_queue.pending_count().await?, 1); + assert!(build_queue.has_build_queued("foo", "0.1.0").await?); Ok(()) }); @@ -536,17 +589,19 @@ mod tests { #[test] fn build_empty_list() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo") .version("0.1.0") .no_builds() - .create()?; + .create_async() + .await?; - let response = env.frontend().get("/crate/foo/0.1.0/builds").send()?; + let response = env.web_app().await.get("/crate/foo/0.1.0/builds").await?; - assert_cache_control(&response, CachePolicy::NoCaching, &env.config()); - let page = kuchikiki::parse_html().one(response.text()?); + response.assert_cache_control(CachePolicy::NoCaching, &env.config()); + let page = kuchikiki::parse_html().one(response.text().await?); let rows: Vec<_> = page .select("ul > li a.release") @@ -571,24 +626,29 @@ mod tests { #[test] fn limits() { - wrapper(|env| { - env.fake_release().name("foo").version("0.1.0").create()?; - - env.runtime().block_on(async { - let mut conn = env.async_db().await.async_conn().await; - let limits = Overrides { - memory: Some(6 * 1024 * 1024 * 1024), - targets: Some(1), - timeout: Some(std::time::Duration::from_secs(2 * 60 * 60)), - }; - Overrides::save(&mut conn, "foo", limits).await - })?; + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("foo") + .version("0.1.0") + .create_async() + .await?; + + let mut conn = env.async_db().await.async_conn().await; + let limits = Overrides { + memory: Some(6 * 1024 * 1024 * 1024), + targets: Some(1), + timeout: Some(std::time::Duration::from_secs(2 * 60 * 60)), + }; + Overrides::save(&mut conn, "foo", limits).await?; let page = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/crate/foo/0.1.0/builds") - .send()? - .text()?, + .await? + .text() + .await?, ); let header = page.select(".about h4").unwrap().next().unwrap(); @@ -614,45 +674,42 @@ mod tests { #[test] fn latest_200() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("aquarelle") .version("0.1.0") .builds(vec![FakeBuild::default() .rustc_version("rustc (blabla 2019-01-01)") .docsrs_version("docs.rs 1.0.0")]) - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("aquarelle") .version("0.2.0") .builds(vec![FakeBuild::default() .rustc_version("rustc (blabla 2019-01-01)") .docsrs_version("docs.rs 1.0.0")]) - .create()?; + .create_async() + .await?; let resp = env - .frontend() + .web_app() + .await .get("/crate/aquarelle/latest/builds") - .send()?; - assert!(resp - .url() - .as_str() - .ends_with("/crate/aquarelle/latest/builds")); - let body = String::from_utf8(resp.bytes().unwrap().to_vec()).unwrap(); + .await?; + let body = resp.text().await?; assert!(body.contains(">>(); - env.fake_release() + env.async_fake_release() + .await .name("library") .version("0.1.0") .features(features) - .create()?; + .create_async() + .await?; let page = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/crate/library/0.1.0/features") - .send()? - .text()?, + .await? + .text() + .await?, ); assert!(page.select_first(r#"p[data-id="empty-features"]"#).is_ok()); Ok(()) @@ -1585,22 +1678,26 @@ mod tests { #[test] fn feature_flags_without_default() { - wrapper(|env| { + async_wrapper(|env| async move { let features = [("feature1".into(), Vec::new())] .iter() .cloned() .collect::>>(); - env.fake_release() + env.async_fake_release() + .await .name("library") .version("0.1.0") .features(features) - .create()?; + .create_async() + .await?; let page = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/crate/library/0.1.0/features") - .send()? - .text()?, + .await? + .text() + .await?, ); assert!(page.select_first(r#"p[data-id="empty-features"]"#).is_err()); let def_len = page @@ -1613,7 +1710,7 @@ mod tests { #[test] fn feature_flags_with_nested_default() { - wrapper(|env| { + async_wrapper(|env| async move { let features = [ ("default".into(), vec!["feature1".into()]), ("feature1".into(), vec!["feature2".into()]), @@ -1622,17 +1719,21 @@ mod tests { .iter() .cloned() .collect::>>(); - env.fake_release() + env.async_fake_release() + .await .name("library") .version("0.1.0") .features(features) - .create()?; + .create_async() + .await?; let page = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/crate/library/0.1.0/features") - .send()? - .text()?, + .await? + .text() + .await?, ); assert!(page.select_first(r#"p[data-id="empty-features"]"#).is_err()); let def_len = page @@ -1645,20 +1746,23 @@ mod tests { #[test] fn details_with_repository_and_stats_can_render_icon() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("library") .version("0.1.0") .repo("https://github.com/org/repo") .github_stats("org/repo", 10, 10, 10) - .create()?; + .create_async() + .await?; let page = kuchikiki::parse_html().one( - env.frontend() - .get("/crate/library/0.1.0/") - .send()? - .error_for_status()? - .text()?, + env.web_app() + .await + .assert_success("/crate/library/0.1.0") + .await? + .text() + .await?, ); let link = page @@ -1683,25 +1787,27 @@ mod tests { #[test] fn feature_flags_report_null() { - wrapper(|env| { + async_wrapper(|env| async move { let id = env - .fake_release() + .async_fake_release() + .await .name("library") .version("0.1.0") - .create()?; + .create_async() + .await?; - env.runtime().block_on(async { - let mut conn = env.async_db().await.async_conn().await; - sqlx::query!("UPDATE releases SET features = NULL WHERE id = $1", id.0) - .execute(&mut *conn) - .await - })?; + let mut conn = env.async_db().await.async_conn().await; + sqlx::query!("UPDATE releases SET features = NULL WHERE id = $1", id.0) + .execute(&mut *conn) + .await?; let page = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/crate/library/0.1.0/features") - .send()? - .text()?, + .await? + .text() + .await?, ); assert!(page.select_first(r#"p[data-id="null-features"]"#).is_ok()); Ok(()) @@ -1710,19 +1816,18 @@ mod tests { #[test] fn test_minimal_failed_release_doesnt_error_features() { - wrapper(|env| { - env.runtime().block_on(async { - let mut conn = env.async_db().await.async_conn().await; - fake_release_that_failed_before_build(&mut conn, "foo", "0.1.0", "some errors") - .await - })?; + async_wrapper(|env| async move { + let mut conn = env.async_db().await.async_conn().await; + fake_release_that_failed_before_build(&mut conn, "foo", "0.1.0", "some errors").await?; let text_content = env - .frontend() + .web_app() + .await .get("/crate/foo/0.1.0/features") - .send()? + .await? .error_for_status()? - .text()?; + .text() + .await?; assert!(text_content.contains( "Feature flags are not available for this release because \ @@ -1735,19 +1840,18 @@ mod tests { #[test] fn test_minimal_failed_release_doesnt_error() { - wrapper(|env| { - env.runtime().block_on(async { - let mut conn = env.async_db().await.async_conn().await; - fake_release_that_failed_before_build(&mut conn, "foo", "0.1.0", "some errors") - .await - })?; + async_wrapper(|env| async move { + let mut conn = env.async_db().await.async_conn().await; + fake_release_that_failed_before_build(&mut conn, "foo", "0.1.0", "some errors").await?; let text_content = env - .frontend() + .web_app() + .await .get("/crate/foo/0.1.0") - .send()? + .await? .error_for_status()? - .text()?; + .text() + .await?; assert!(text_content.contains("docs.rs failed to build foo")); @@ -1791,10 +1895,21 @@ mod tests { platform_links } - fn run_check_links_redir(env: &TestEnvironment, url: &str, should_contain_redirect: bool) { - let response = env.frontend().get(url).send().unwrap(); - assert!(response.status().is_success()); - let text = response.text().unwrap(); + async fn run_check_links_redir( + env: &TestEnvironment, + url: &str, + should_contain_redirect: bool, + ) { + let response = env.web_app().await.get(url).await.unwrap(); + let status = response.status(); + assert!( + status.is_success(), + "no success, status: {}, url: {}, target: {}", + status, + url, + response.redirect_target().unwrap_or_default(), + ); + let text = response.text().await.unwrap(); let list1 = check_links(text.clone(), false, should_contain_redirect); // Same test with AJAX endpoint. @@ -1807,15 +1922,20 @@ mod tests { .get("data-url") .expect("data-url") .to_string(); - let response = env.frontend().get(&platform_menu_url).send().unwrap(); + let response = env.web_app().await.get(&platform_menu_url).await.unwrap(); assert!(response.status().is_success()); - assert_cache_control(&response, CachePolicy::ForeverInCdn, &env.config()); - let list2 = check_links(response.text().unwrap(), true, should_contain_redirect); + response.assert_cache_control(CachePolicy::ForeverInCdn, &env.config()); + let list2 = check_links( + response.text().await.unwrap(), + true, + should_contain_redirect, + ); assert_eq!(list1, list2); } - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("0.4.0") .rustdoc_file("dummy/index.html") @@ -1824,21 +1944,28 @@ mod tests { .default_target("x86_64-unknown-linux-gnu") .add_target("x86_64-pc-windows-msvc") .source_file("README.md", b"storage readme") - .create()?; + .create_async() + .await?; - run_check_links_redir(env, "/crate/dummy/0.4.0/features", false); - run_check_links_redir(env, "/crate/dummy/0.4.0/builds", false); - run_check_links_redir(env, "/crate/dummy/0.4.0/source/", false); - run_check_links_redir(env, "/crate/dummy/0.4.0/source/README.md", false); - run_check_links_redir(env, "/crate/dummy/0.4.0", false); + run_check_links_redir(&env, "/crate/dummy/0.4.0/features", false).await; + run_check_links_redir(&env, "/crate/dummy/0.4.0/builds", false).await; + run_check_links_redir(&env, "/crate/dummy/0.4.0/source/", false).await; + run_check_links_redir(&env, "/crate/dummy/0.4.0/source/README.md", false).await; + run_check_links_redir(&env, "/crate/dummy/0.4.0", false).await; - run_check_links_redir(env, "/dummy/latest/dummy", true); - run_check_links_redir(env, "/dummy/0.4.0/x86_64-pc-windows-msvc/dummy", true); + run_check_links_redir(&env, "/dummy/latest/dummy/", true).await; run_check_links_redir( - env, + &env, + "/dummy/0.4.0/x86_64-pc-windows-msvc/dummy/index.html", + true, + ) + .await; + run_check_links_redir( + &env, "/dummy/0.4.0/x86_64-pc-windows-msvc/dummy/struct.A.html", true, - ); + ) + .await; Ok(()) }); @@ -1846,12 +1973,12 @@ mod tests { #[test] fn check_crate_name_in_redirect() { - fn check_links(env: &TestEnvironment, url: &str, links: Vec) { - let response = env.frontend().get(url).send().unwrap(); + async fn check_links(env: &TestEnvironment, url: &str, links: Vec) { + let response = env.web_app().await.get(url).await.unwrap(); assert!(response.status().is_success()); let platform_links: Vec = kuchikiki::parse_html() - .one(response.text().unwrap()) + .one(response.text().await.unwrap()) .select("li a") .expect("invalid selector") .map(|el| { @@ -1864,39 +1991,44 @@ mod tests { assert_eq!(platform_links, links,); } - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy-ba") .version("0.4.0") .rustdoc_file("dummy-ba/index.html") .rustdoc_file("x86_64-unknown-linux-gnu/dummy-ba/index.html") .add_target("x86_64-unknown-linux-gnu") - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("dummy-ba") .version("0.5.0") .rustdoc_file("dummy-ba/index.html") .rustdoc_file("x86_64-unknown-linux-gnu/dummy-ba/index.html") .add_target("x86_64-unknown-linux-gnu") - .create()?; + .create_async() + .await?; check_links( - env, + &env, "/crate/dummy-ba/latest/menus/releases/dummy_ba/index.html", vec![ "/crate/dummy-ba/0.5.0/target-redirect/dummy_ba/index.html".to_string(), "/crate/dummy-ba/0.4.0/target-redirect/dummy_ba/index.html".to_string(), ], - ); + ) + .await; check_links( - env, + &env, "/crate/dummy-ba/latest/menus/releases/x86_64-unknown-linux-gnu/dummy_ba/index.html", vec![ "/crate/dummy-ba/0.5.0/target-redirect/x86_64-unknown-linux-gnu/dummy_ba/index.html".to_string(), "/crate/dummy-ba/0.4.0/target-redirect/x86_64-unknown-linux-gnu/dummy_ba/index.html".to_string(), ], - ); + ).await; Ok(()) }); @@ -1910,9 +2042,10 @@ mod tests { assert!(crate::DEFAULT_MAX_TARGETS > 2); fn check_count(nb_targets: usize, expected: usize) { - wrapper(|env| { + async_wrapper(|env| async move { let mut rel = env - .fake_release() + .async_fake_release() + .await .name("dummy") .version("0.4.0") .rustdoc_file("dummy/index.html") @@ -1922,13 +2055,13 @@ mod tests { for nb in 0..nb_targets - 1 { rel = rel.add_target(&format!("x86_64-pc-windows-msvc{nb}")); } - rel.create()?; + rel.create_async().await?; - let response = env.frontend().get("/crate/dummy/0.4.0").send()?; + let response = env.web_app().await.get("/crate/dummy/0.4.0").await?; assert!(response.status().is_success()); let nb_li = kuchikiki::parse_html() - .one(response.text()?) + .one(response.text().await?) .select(r#"#platforms li a"#) .expect("invalid selector") .count(); @@ -1946,44 +2079,37 @@ mod tests { #[test] fn latest_url() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("0.4.0") .rustdoc_file("dummy/index.html") .rustdoc_file("x86_64-pc-windows-msvc/dummy/index.html") .default_target("x86_64-unknown-linux-gnu") .add_target("x86_64-pc-windows-msvc") - .create()?; - let web = env.frontend(); + .create_async() + .await?; + let web = env.web_app().await; - let resp = env.frontend().get("/crate/dummy/latest").send()?; + let resp = web.get("/crate/dummy/latest").await?; assert!(resp.status().is_success()); - assert_cache_control(&resp, CachePolicy::ForeverInCdn, &env.config()); - assert!(resp.url().as_str().ends_with("/crate/dummy/latest")); - let body = String::from_utf8(resp.bytes().unwrap().to_vec()).unwrap(); + resp.assert_cache_control(CachePolicy::ForeverInCdn, &env.config()); + let body = resp.text().await?; assert!(body.contains(" = Result; #[cfg(test)] mod tests { use super::{AxumNope, IntoResponse}; - use crate::{test::wrapper, web::cache::CachePolicy}; + use crate::test::{async_wrapper, AxumResponseTestExt, AxumRouterTestExt}; + use crate::web::cache::CachePolicy; use kuchikiki::traits::TendrilSink; #[test] @@ -232,12 +233,14 @@ mod tests { #[test] fn check_404_page_content_crate() { - wrapper(|env| { + async_wrapper(|env| async move { let page = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/crate-which-doesnt-exist") - .send()? - .text()?, + .await? + .text() + .await?, ); assert_eq!(page.select("#crate-title").unwrap().count(), 1); assert_eq!( @@ -255,12 +258,14 @@ mod tests { #[test] fn check_404_page_content_resource() { - wrapper(|env| { + async_wrapper(|env| async move { let page = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/resource-which-doesnt-exist.js") - .send()? - .text()?, + .await? + .text() + .await?, ); assert_eq!(page.select("#crate-title").unwrap().count(), 1); assert_eq!( @@ -278,13 +283,17 @@ mod tests { #[test] fn check_400_page_content_not_semver_version() { - wrapper(|env| { - env.fake_release().name("dummy").create()?; + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("dummy") + .create_async() + .await?; - let response = env.frontend().get("/dummy/not-semver").send()?; + let response = env.web_app().await.get("/dummy/not-semver").await?; assert_eq!(response.status(), 400); - let page = kuchikiki::parse_html().one(response.text()?); + let page = kuchikiki::parse_html().one(response.text().await?); assert_eq!(page.select("#crate-title").unwrap().count(), 1); assert_eq!( page.select("#crate-title") @@ -301,10 +310,15 @@ mod tests { #[test] fn check_404_page_content_nonexistent_version() { - wrapper(|env| { - env.fake_release().name("dummy").version("1.0.0").create()?; - let page = - kuchikiki::parse_html().one(env.frontend().get("/dummy/2.0").send()?.text()?); + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("dummy") + .version("1.0.0") + .create_async() + .await?; + let page = kuchikiki::parse_html() + .one(env.web_app().await.get("/dummy/2.0").await?.text().await?); assert_eq!(page.select("#crate-title").unwrap().count(), 1); assert_eq!( page.select("#crate-title") @@ -321,13 +335,16 @@ mod tests { #[test] fn check_404_page_content_any_version_all_yanked() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("1.0.0") .yanked(true) - .create()?; - let page = kuchikiki::parse_html().one(env.frontend().get("/dummy/*").send()?.text()?); + .create_async() + .await?; + let page = kuchikiki::parse_html() + .one(env.web_app().await.get("/dummy/*").await?.text().await?); assert_eq!(page.select("#crate-title").unwrap().count(), 1); assert_eq!( page.select("#crate-title") diff --git a/src/web/features.rs b/src/web/features.rs index 2f23f155c..acbededa2 100644 --- a/src/web/features.rs +++ b/src/web/features.rs @@ -232,7 +232,7 @@ fn get_sorted_features(raw_features: Vec) -> (Vec, HashSet() .and_then(|io| io.get_ref()) @@ -135,13 +141,13 @@ mod tests { .is_some() }; - assert_len(MAX_HTML_SIZE / 2, "small.html"); - assert_len(MAX_HTML_SIZE, "exact.html"); - assert_len(MAX_SIZE / 2, "small.js"); - assert_len(MAX_SIZE, "exact.js"); + assert_len(MAX_HTML_SIZE / 2, "small.html").await; + assert_len(MAX_HTML_SIZE, "exact.html").await; + assert_len(MAX_SIZE / 2, "small.js").await; + assert_len(MAX_SIZE, "exact.js").await; - assert_too_big("big.html"); - assert_too_big("big.js"); + assert_too_big("big.html").await; + assert_too_big("big.js").await; Ok(()) }) diff --git a/src/web/metrics.rs b/src/web/metrics.rs index e371f89d3..ef597377b 100644 --- a/src/web/metrics.rs +++ b/src/web/metrics.rs @@ -110,14 +110,13 @@ pub(crate) async fn request_recorder( #[cfg(test)] mod tests { - use crate::test::wrapper; + use crate::test::{async_wrapper, AxumResponseTestExt, AxumRouterTestExt}; use crate::Context; use std::collections::HashMap; #[test] fn test_response_times_count_being_collected() { const ROUTES: &[(&str, &str)] = &[ - ("", "/"), ("/", "/"), ("/crate/hexponent/0.2.0", "/crate/:name/:version"), ("/crate/rcc/0.0.0", "/crate/:name/:version"), @@ -152,28 +151,34 @@ mod tests { ("/rustdoc/gcc/0.0.0/gcc/index.html", "rustdoc page"), ]; - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("rcc") .version("0.0.0") .repo("https://github.com/jyn514/rcc") - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("rcc") .version("1.0.0") .build_result_failed() - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("hexponent") .version("0.2.0") - .create()?; + .create_async() + .await?; - let frontend = env.frontend(); + let frontend = env.web_app().await; let metrics = env.instance_metrics(); for (route, _) in ROUTES.iter() { - frontend.get(route).send()?; - frontend.get(route).send()?; + frontend.get(route).await?; + frontend.get(route).await?; } let mut expected = HashMap::new(); @@ -183,7 +188,7 @@ mod tests { } // this shows what the routes were *actually* recorded as, making it easier to update ROUTES if the name changes. - let metrics_serialized = metrics.gather(&env.pool()?)?; + let metrics_serialized = metrics.gather(&env.async_pool().await?)?; let all_routes_visited = metrics_serialized .iter() .find(|x| x.get_name() == "docsrs_routes_visited") @@ -223,11 +228,11 @@ mod tests { #[test] fn test_metrics_page_success() { - wrapper(|env| { - let response = env.frontend().get("/about/metrics").send()?; + async_wrapper(|env| async move { + let response = env.web_app().await.get("/about/metrics").await?; assert!(response.status().is_success()); - let body = response.text()?; + let body = response.text().await?; assert!(body.contains("docsrs_failed_builds"), "{}", body); assert!(body.contains("queued_crates_count"), "{}", body); Ok(()) @@ -236,11 +241,11 @@ mod tests { #[test] fn test_service_metrics_page_success() { - wrapper(|env| { - let response = env.frontend().get("/about/metrics/service").send()?; + async_wrapper(|env| async move { + let response = env.web_app().await.get("/about/metrics/service").await?; assert!(response.status().is_success()); - let body = response.text()?; + let body = response.text().await?; assert!(!body.contains("docsrs_failed_builds"), "{}", body); assert!(body.contains("queued_crates_count"), "{}", body); Ok(()) @@ -249,11 +254,11 @@ mod tests { #[test] fn test_instance_metrics_page_success() { - wrapper(|env| { - let response = env.frontend().get("/about/metrics/instance").send()?; + async_wrapper(|env| async move { + let response = env.web_app().await.get("/about/metrics/instance").await?; assert!(response.status().is_success()); - let body = response.text()?; + let body = response.text().await?; assert!(body.contains("docsrs_failed_builds"), "{}", body); assert!(!body.contains("queued_crates_count"), "{}", body); Ok(()) diff --git a/src/web/mod.rs b/src/web/mod.rs index b6720ead2..50d7fbdd9 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -767,7 +767,11 @@ impl_axum_webpage! { #[cfg(test)] mod test { use super::*; - use crate::{db::ReleaseId, docbuilder::DocCoverage, test::*}; + use crate::test::{ + async_wrapper, AxumResponseTestExt, AxumRouterTestExt, FakeBuild, TestDatabase, + TestEnvironment, + }; + use crate::{db::ReleaseId, docbuilder::DocCoverage}; use kuchikiki::traits::TendrilSink; use serde_json::json; use test_case::test_case; @@ -807,25 +811,26 @@ mod test { version.parse().ok() } - fn clipboard_is_present_for_path(path: &str, web: &TestFrontend) -> bool { - let data = web.get(path).send().unwrap().text().unwrap(); + async fn clipboard_is_present_for_path(path: &str, web: &axum::Router) -> bool { + let data = web.get(path).await.unwrap().text().await.unwrap(); let node = kuchikiki::parse_html().one(data); node.select("#clipboard").unwrap().count() == 1 } #[test] fn test_index_returns_success() { - wrapper(|env| { - let web = env.frontend(); - assert!(web.get("/").send()?.status().is_success()); + async_wrapper(|env| async move { + let web = env.web_app().await; + assert!(web.get("/").await?.status().is_success()); Ok(()) }); } #[test] fn test_doc_coverage_for_crate_pages() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo") .version("0.0.1") .source_file("test.rs", &[]) @@ -835,22 +840,23 @@ mod test { total_items_needing_examples: 2, items_with_examples: 1, }) - .create()?; - let web = env.frontend(); + .create_async() + .await?; + let web = env.web_app().await; + + let foo_crate = kuchikiki::parse_html() + .one(web.assert_success("/crate/foo/0.0.1").await?.text().await?); - let foo_crate = - kuchikiki::parse_html().one(web.get("/crate/foo/0.0.1").send()?.text()?); for (idx, value) in ["60%", "6", "10", "2", "1"].iter().enumerate() { + let mut menu_items = foo_crate.select(".pure-menu-item b").unwrap(); assert!( - foo_crate - .select(".pure-menu-item b") - .unwrap() - .any(|e| dbg!(e.text_contents()).contains(value)), + menu_items.any(|e| dbg!(e.text_contents()).contains(value)), "({idx}, {value:?})" ); } - let foo_doc = kuchikiki::parse_html().one(web.get("/foo/0.0.1/foo").send()?.text()?); + let foo_doc = kuchikiki::parse_html() + .one(web.assert_success("/foo/0.0.1/foo/").await?.text().await?); assert!(foo_doc .select(".pure-menu-link b") .unwrap() @@ -862,80 +868,92 @@ mod test { #[test] fn test_show_clipboard_for_crate_pages() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("fake_crate") .version("0.0.1") .source_file("test.rs", &[]) - .create() - .unwrap(); - let web = env.frontend(); - assert!(clipboard_is_present_for_path( - "/crate/fake_crate/0.0.1", - web - )); - assert!(clipboard_is_present_for_path( - "/crate/fake_crate/0.0.1/source/", - web - )); - assert!(clipboard_is_present_for_path( - "/fake_crate/0.0.1/fake_crate", - web - )); + .create_async() + .await?; + let web = env.web_app().await; + assert!(clipboard_is_present_for_path("/crate/fake_crate/0.0.1", &web).await); + assert!(clipboard_is_present_for_path("/crate/fake_crate/0.0.1/source/", &web).await); + assert!(clipboard_is_present_for_path("/fake_crate/0.0.1/fake_crate/", &web).await); Ok(()) }); } #[test] fn test_hide_clipboard_for_non_crate_pages() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("fake_crate") .version("0.0.1") - .create() - .unwrap(); - let web = env.frontend(); - assert!(!clipboard_is_present_for_path("/about", web)); - assert!(!clipboard_is_present_for_path("/releases", web)); - assert!(!clipboard_is_present_for_path("/", web)); - assert!(!clipboard_is_present_for_path("/not/a/real/path", web)); + .create_async() + .await?; + let web = env.web_app().await; + assert!(!clipboard_is_present_for_path("/about", &web).await); + assert!(!clipboard_is_present_for_path("/releases", &web).await); + assert!(!clipboard_is_present_for_path("/", &web).await); + assert!(!clipboard_is_present_for_path("/not/a/real/path", &web).await); Ok(()) }); } #[test] fn standard_library_redirects() { - wrapper(|env| { - let web = env.frontend(); + async fn assert_external_redirect_success( + web: &axum::Router, + path: &str, + expected_target: &str, + ) -> Result<()> { + let redirect_response = web.assert_redirect_unchecked(path, expected_target).await?; + + let external_target_url = redirect_response.redirect_target().unwrap(); + + let response = reqwest::get(external_target_url).await?; + let status = response.status(); + assert!( + status.is_success(), + "failed to GET {external_target_url}: {status}" + ); + Ok(()) + } + + async_wrapper(|env| async move { + let web = env.web_app().await; for krate in &["std", "alloc", "core", "proc_macro", "test"] { let target = format!("https://doc.rust-lang.org/stable/{krate}/"); // with or without slash - assert_redirect(&format!("/{krate}"), &target, web)?; - assert_redirect(&format!("/{krate}/"), &target, web)?; + assert_external_redirect_success(&web, &format!("/{krate}"), &target).await?; + assert_external_redirect_success(&web, &format!("/{krate}/"), &target).await?; } let target = "https://doc.rust-lang.org/stable/proc_macro/"; // with or without slash - assert_redirect("/proc-macro", target, web)?; - assert_redirect("/proc-macro/", target, web)?; + assert_external_redirect_success(&web, "/proc-macro", target).await?; + assert_external_redirect_success(&web, "/proc-macro/", target).await?; let target = "https://doc.rust-lang.org/nightly/nightly-rustc/"; // with or without slash - assert_redirect("/rustc", target, web)?; - assert_redirect("/rustc/", target, web)?; + assert_external_redirect_success(&web, "/rustc", target).await?; + assert_external_redirect_success(&web, "/rustc/", target).await?; let target = "https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc/"; // with or without slash - assert_redirect("/rustdoc", target, web)?; - assert_redirect("/rustdoc/", target, web)?; + assert_external_redirect_success(&web, "/rustdoc", target).await?; + assert_external_redirect_success(&web, "/rustdoc/", target).await?; // queries are supported - assert_redirect( + assert_external_redirect_success( + &web, "/std?search=foobar", "https://doc.rust-lang.org/stable/std/?search=foobar", - web, - )?; + ) + .await?; Ok(()) }) @@ -943,30 +961,34 @@ mod test { #[test] fn double_slash_does_redirect_to_latest_version() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("bat") .version("0.2.0") - .create() - .unwrap(); - let web = env.frontend(); - assert_redirect("/bat//", "/bat/latest/bat/", web)?; + .create_async() + .await?; + let web = env.web_app().await; + web.assert_redirect("/bat//", "/bat/latest/bat/").await?; Ok(()) }) } #[test] fn binary_docs_redirect_to_crate() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("bat") .version("0.2.0") .binary(true) - .create() - .unwrap(); - let web = env.frontend(); - assert_redirect("/bat/0.2.0", "/crate/bat/0.2.0", web)?; - assert_redirect("/bat/0.2.0/i686-unknown-linux-gnu", "/crate/bat/0.2.0", web)?; + .create_async() + .await?; + let web = env.web_app().await; + web.assert_redirect("/bat/0.2.0", "/crate/bat/0.2.0") + .await?; + web.assert_redirect("/bat/0.2.0/i686-unknown-linux-gnu", "/crate/bat/0.2.0") + .await?; /* TODO: this should work (https://github.com/rust-lang/docs.rs/issues/603) assert_redirect("/bat/0.2.0/i686-unknown-linux-gnu/bat", "/crate/bat/0.2.0", web)?; assert_redirect("/bat/0.2.0/i686-unknown-linux-gnu/bat/", "/crate/bat/0.2.0/", web)?; @@ -977,19 +999,22 @@ mod test { #[test] fn can_view_source() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("regex") .version("0.3.0") .source_file("src/main.rs", br#"println!("definitely valid rust")"#) - .create() - .unwrap(); - - let web = env.frontend(); - assert_success("/crate/regex/0.3.0/source/src/main.rs", web)?; - assert_success("/crate/regex/0.3.0/source", web)?; - assert_success("/crate/regex/0.3.0/source/src", web)?; - assert_success("/regex/0.3.0/src/regex/main.rs.html", web)?; + .create_async() + .await?; + + let web = env.web_app().await; + web.assert_success("/crate/regex/0.3.0/source/src/main.rs") + .await?; + web.assert_success("/crate/regex/0.3.0/source/").await?; + web.assert_success("/crate/regex/0.3.0/source/src").await?; + web.assert_success("/regex/0.3.0/src/regex/main.rs.html") + .await?; Ok(()) }) } @@ -1024,10 +1049,10 @@ mod test { #[test] fn platform_dropdown_not_shown_with_no_targets() { - wrapper(|env| { - env.runtime().block_on(release("0.1.0", env)); - let web = env.frontend(); - let text = web.get("/foo/0.1.0/foo").send()?.text()?; + async_wrapper(|env| async move { + release("0.1.0", &env).await; + let web = env.web_app().await; + let text = web.get("/foo/0.1.0/foo").await?.text().await?; let platform = kuchikiki::parse_html() .one(text) .select(r#"ul > li > a[aria-label="Platform"]"#) @@ -1036,12 +1061,14 @@ mod test { assert_eq!(platform, 0); // sanity check the test is doing something - env.fake_release() + env.async_fake_release() + .await .name("foo") .version("0.2.0") .add_platform("x86_64-unknown-linux-musl") - .create()?; - let text = web.get("/foo/0.2.0/foo").send()?.text()?; + .create_async() + .await?; + let text = web.assert_success("/foo/0.2.0/foo/").await?.text().await?; let platform = kuchikiki::parse_html() .one(text) .select(r#"ul > li > a[aria-label="Platform"]"#) @@ -1261,10 +1288,10 @@ mod test { #[test] fn test_tabindex_is_present_on_topbar_crate_search_input() { - wrapper(|env| { - env.runtime().block_on(release("0.1.0", env)); - let web = env.frontend(); - let text = web.get("/foo/0.1.0/foo").send()?.text()?; + async_wrapper(|env| async move { + release("0.1.0", &env).await; + let web = env.web_app().await; + let text = web.assert_success("/foo/0.1.0/foo/").await?.text().await?; let tabindex = kuchikiki::parse_html() .one(text) .select(r#"#nav-search[tabindex="-1"]"#) diff --git a/src/web/releases.rs b/src/web/releases.rs index e9c332169..f7e1b7c4b 100644 --- a/src/web/releases.rs +++ b/src/web/releases.rs @@ -787,8 +787,8 @@ mod tests { use crate::db::{finish_build, initialize_build, initialize_crate, initialize_release}; use crate::registry_api::{CrateOwner, OwnerKind}; use crate::test::{ - assert_cache_control, assert_redirect, assert_redirect_unchecked, assert_success, - async_wrapper, fake_release_that_failed_before_build, wrapper, FakeBuild, TestFrontend, + async_wrapper, fake_release_that_failed_before_build, AxumResponseTestExt, + AxumRouterTestExt, FakeBuild, }; use anyhow::Error; use chrono::{Duration, TimeZone}; @@ -835,43 +835,54 @@ mod tests { #[test] fn get_releases_by_stars() { - wrapper(|env| { - let db = env.db(); + async_wrapper(|env| async move { + let db = env.async_db().await; - env.fake_release() + env.async_fake_release() + .await .name("foo") .version("1.0.0") .github_stats("ghost/foo", 10, 10, 10) - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("bar") .version("1.0.0") .github_stats("ghost/bar", 20, 20, 20) - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("bar") .version("1.0.0") .github_stats("ghost/bar", 20, 20, 20) - .create()?; + .create_async() + .await?; // release without stars will not be shown - env.fake_release().name("baz").version("1.0.0").create()?; + env.async_fake_release() + .await + .name("baz") + .version("1.0.0") + .create_async() + .await?; // release with only in-progress build (= in progress release) will not be shown - env.fake_release() + env.async_fake_release() + .await .name("in_progress") .version("0.1.0") .builds(vec![FakeBuild::default() .build_status(BuildStatus::InProgress) .rustc_version("rustc (blabla 2022-01-01)") .docsrs_version("docs.rs 4.0.0")]) - .create()?; + .create_async() + .await?; - let releases = env - .runtime() - .block_on(async move { - get_releases(&mut *db.async_conn().await, 1, 10, Order::GithubStars, true).await - }) - .unwrap(); + let releases = + get_releases(&mut *db.async_conn().await, 1, 10, Order::GithubStars, true) + .await + .unwrap(); assert_eq!( vec![ "bar", // 20 stars @@ -889,116 +900,146 @@ mod tests { #[test] fn search_im_feeling_lucky_with_query_redirect_to_crate_page() { - wrapper(|env| { - let web = env.frontend(); - env.fake_release() + async_wrapper(|env| async move { + let web = env.web_app().await; + env.async_fake_release() + .await .name("some_random_crate") .build_result_failed() - .create()?; - env.fake_release().name("some_other_crate").create()?; + .create_async() + .await?; + env.async_fake_release() + .await + .name("some_other_crate") + .create_async() + .await?; - assert_redirect( + web.assert_redirect( "/releases/search?query=some_random_crate&i-am-feeling-lucky=1", "/crate/some_random_crate/1.0.0", - web, - )?; + ) + .await?; Ok(()) }) } #[test] fn search_im_feeling_lucky_with_query_redirect_to_docs() { - wrapper(|env| { - let web = env.frontend(); - env.fake_release().name("some_random_crate").create()?; - env.fake_release().name("some_other_crate").create()?; + async_wrapper(|env| async move { + let web = env.web_app().await; + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; + env.async_fake_release() + .await + .name("some_other_crate") + .create_async() + .await?; - assert_redirect( + web.assert_redirect( "/releases/search?query=some_random_crate&i-am-feeling-lucky=1", "/some_random_crate/1.0.0/some_random_crate/", - web, - )?; + ) + .await?; Ok(()) }) } #[test] fn im_feeling_lucky_with_stars() { - wrapper(|env| { - env.runtime().block_on(async { - // The normal test-setup will offset all primary sequences by 10k - // to prevent errors with foreign key relations. - // Random-crate-search relies on the sequence for the crates-table - // to find a maximum possible ID. This combined with only one actual - // crate in the db breaks this test. - // That's why we reset the id-sequence to zero for this test. - - let mut conn = env.async_db().await.async_conn().await; - sqlx::query!(r#"ALTER SEQUENCE crates_id_seq RESTART WITH 1"#) - .execute(&mut *conn) - .await - })?; - - let web = env.frontend(); - env.fake_release() + async_wrapper(|env| async move { + // The normal test-setup will offset all primary sequences by 10k + // to prevent errors with foreign key relations. + // Random-crate-search relies on the sequence for the crates-table + // to find a maximum possible ID. This combined with only one actual + // crate in the db breaks this test. + // That's why we reset the id-sequence to zero for this test. + + let mut conn = env.async_db().await.async_conn().await; + sqlx::query!(r#"ALTER SEQUENCE crates_id_seq RESTART WITH 1"#) + .execute(&mut *conn) + .await?; + + let web = env.web_app().await; + env.async_fake_release() + .await .github_stats("some/repo", 333, 22, 11) .name("some_random_crate") - .create()?; - assert_redirect( + .create_async() + .await?; + web.assert_redirect( "/releases/search?query=&i-am-feeling-lucky=1", "/some_random_crate/1.0.0/some_random_crate/", - web, - )?; + ) + .await?; Ok(()) }) } #[test] fn search_coloncolon_path_redirects_to_crate_docs() { - wrapper(|env| { - let web = env.frontend(); - env.fake_release().name("some_random_crate").create()?; - env.fake_release().name("some_other_crate").create()?; + async_wrapper(|env| async move { + let web = env.web_app().await; + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; + env.async_fake_release() + .await + .name("some_other_crate") + .create_async() + .await?; - assert_redirect( + web.assert_redirect( "/releases/search?query=some_random_crate::somepath", "/some_random_crate/1.0.0/some_random_crate/?search=somepath", - web, - )?; - assert_redirect( + ) + .await?; + web.assert_redirect( "/releases/search?query=some_random_crate::some::path", "/some_random_crate/1.0.0/some_random_crate/?search=some%3A%3Apath", - web, - )?; + ) + .await?; Ok(()) }) } #[test] fn search_coloncolon_path_redirects_to_crate_docs_and_keeps_query() { - wrapper(|env| { - let web = env.frontend(); - env.fake_release().name("some_random_crate").create()?; + async_wrapper(|env| async move { + let web = env.web_app().await; + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; - assert_redirect( + web.assert_redirect( "/releases/search?query=some_random_crate::somepath&go_to_first=true", "/some_random_crate/1.0.0/some_random_crate/?go_to_first=true&search=somepath", - web, - )?; + ) + .await?; Ok(()) }) } #[test] - fn search_result_can_retrieve_sort_by_from_pagination() { - wrapper(|env| { - let mut crates_io = mockito::Server::new(); + fn search_result_can_retrive_sort_by_from_pagination() { + async_wrapper(|env| async move { + let mut crates_io = mockito::Server::new_async().await; env.override_config(|config| { config.registry_api_host = crates_io.url().parse().unwrap(); }); - let web = env.frontend(); - env.fake_release().name("some_random_crate").create()?; + let web = env.web_app().await; + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; let _m = crates_io .mock("GET", "/api/v1/crates") @@ -1022,17 +1063,17 @@ mod tests { }) .to_string(), ) - .create(); + .create_async().await; // click the "Next Page" Button, the "Sort by" SelectBox should keep the same option. let next_page_url = format!( "/releases/search?paginate={}", b64.encode("?q=some_random_crate&sort=recent-updates&per_page=30&page=2"), ); - let response = web.get(&next_page_url).send()?; + let response = web.get(&next_page_url).await?; assert!(response.status().is_success()); - let page = kuchikiki::parse_html().one(response.text()?); + let page = kuchikiki::parse_html().one(response.text().await?); let is_target_option_selected = page .select("#nav-sort > option") .expect("missing option") @@ -1049,14 +1090,18 @@ mod tests { #[test] fn search_result_passes_cratesio_pagination_links() { - wrapper(|env| { - let mut crates_io = mockito::Server::new(); + async_wrapper(|env| async move { + let mut crates_io = mockito::Server::new_async().await; env.override_config(|config| { config.registry_api_host = crates_io.url().parse().unwrap(); }); - let web = env.frontend(); - env.fake_release().name("some_random_crate").create()?; + let web = env.web_app().await; + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; let _m = crates_io .mock("GET", "/api/v1/crates") @@ -1078,12 +1123,13 @@ mod tests { }) .to_string(), ) - .create(); + .create_async() + .await; - let response = web.get("/releases/search?query=some_random_crate").send()?; + let response = web.get("/releases/search?query=some_random_crate").await?; assert!(response.status().is_success()); - let page = kuchikiki::parse_html().one(response.text()?); + let page = kuchikiki::parse_html().one(response.text().await?); let other_search_links: Vec<_> = page .select("a") @@ -1117,14 +1163,15 @@ mod tests { #[test] fn search_invalid_paginate_doesnt_request_cratesio() { - wrapper(|env| { + async_wrapper(|env| async move { let response = env - .frontend() + .web_app() + .await .get(&format!( "/releases/search?paginate={}", b64.encode("something_that_doesnt_start_with_?") )) - .send()?; + .await?; assert_eq!(response.status(), StatusCode::NOT_FOUND); Ok(()) }) @@ -1132,8 +1179,8 @@ mod tests { #[test] fn crates_io_errors_as_status_code_200() { - wrapper(|env| { - let mut crates_io = mockito::Server::new(); + async_wrapper(|env| async move { + let mut crates_io = mockito::Server::new_async().await; env.override_config(|config| { config.crates_io_api_call_retries = 0; config.registry_api_host = crates_io.url().parse().unwrap(); @@ -1156,15 +1203,20 @@ mod tests { }) .to_string(), ) - .create(); + .create_async() + .await; let response = env - .frontend() + .web_app() + .await .get("/releases/search?query=doesnt_matter_here") - .send()?; + .await?; assert_eq!(response.status(), 500); - assert!(response.text()?.contains("error name 1\nerror name 2")); + assert!(response + .text() + .await? + .contains("error name 1\nerror name 2")); Ok(()) }) } @@ -1173,8 +1225,8 @@ mod tests { #[test_case(StatusCode::INTERNAL_SERVER_ERROR)] #[test_case(StatusCode::BAD_GATEWAY)] fn crates_io_errors_are_correctly_returned_and_we_dont_try_parsing(status: StatusCode) { - wrapper(|env| { - let mut crates_io = mockito::Server::new(); + async_wrapper(|env| async move { + let mut crates_io = mockito::Server::new_async().await; env.override_config(|config| { config.crates_io_api_call_retries = 0; config.registry_api_host = crates_io.url().parse().unwrap(); @@ -1187,29 +1239,35 @@ mod tests { Matcher::UrlEncoded("per_page".into(), "30".into()), ])) .with_status(status.as_u16() as usize) - .create(); + .create_async() + .await; let response = env - .frontend() + .web_app() + .await .get("/releases/search?query=doesnt_matter_here") - .send()?; + .await?; assert_eq!(response.status(), 500); - assert!(response.text()?.contains(&format!("{status}"))); + assert!(response.text().await?.contains(&format!("{status}"))); Ok(()) }) } #[test] fn search_encoded_pagination_passed_to_cratesio() { - wrapper(|env| { - let mut crates_io = mockito::Server::new(); + async_wrapper(|env| async move { + let mut crates_io = mockito::Server::new_async().await; env.override_config(|config| { config.registry_api_host = crates_io.url().parse().unwrap(); }); - let web = env.frontend(); - env.fake_release().name("some_random_crate").create()?; + let web = env.web_app().await; + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; let _m = crates_io .mock("GET", "/api/v1/crates") @@ -1231,15 +1289,17 @@ mod tests { }) .to_string(), ) - .create(); + .create_async() + .await; let links = get_release_links( &format!( "/releases/search?paginate={}", b64.encode("?some=dummy&pagination=parameters") ), - web, - )?; + &web, + ) + .await?; assert_eq!(links.len(), 1); assert_eq!(links[0], "/some_random_crate/latest/some_random_crate/",); @@ -1249,14 +1309,18 @@ mod tests { #[test] fn search_lucky_with_unknown_crate() { - wrapper(|env| { - let mut crates_io = mockito::Server::new(); + async_wrapper(|env| async move { + let mut crates_io = mockito::Server::new_async().await; env.override_config(|config| { config.registry_api_host = crates_io.url().parse().unwrap(); }); - let web = env.frontend(); - env.fake_release().name("some_random_crate").create()?; + let web = env.web_app().await; + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; let _m = crates_io .mock("GET", "/api/v1/crates") @@ -1279,14 +1343,16 @@ mod tests { }) .to_string(), ) - .create(); + .create_async() + .await; // when clicking "I'm feeling lucky" and the query doesn't match any crate, // just fallback to the normal search results. let links = get_release_links( "/releases/search?query=some_random_&i-am-feeling-lucky=1", - web, - )?; + &web, + ) + .await?; assert_eq!(links.len(), 1); assert_eq!(links[0], "/some_random_crate/latest/some_random_crate/"); @@ -1296,54 +1362,62 @@ mod tests { #[test] fn search() { - wrapper(|env| { - let mut crates_io = mockito::Server::new(); + async_wrapper(|env| async move { + let mut crates_io = mockito::Server::new_async().await; env.override_config(|config| { config.registry_api_host = crates_io.url().parse().unwrap(); }); - let web = env.frontend(); - env.fake_release() + let web = env.web_app().await; + env.async_fake_release() + .await .name("some_random_crate") .version("2.0.0") - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("some_random_crate") .version("1.0.0") - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("and_another_one") .version("0.0.1") - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("yet_another_crate") .version("0.1.0") .yanked(true) - .create()?; + .create_async() + .await?; // release with only in-progress build (= in progress release) will not be shown - env.fake_release() + env.async_fake_release() + .await .name("in_progress") .version("0.1.0") .builds(vec![FakeBuild::default() .build_status(BuildStatus::InProgress) .rustc_version("rustc (blabla 2022-01-01)") .docsrs_version("docs.rs 4.0.0")]) - .create()?; + .create_async() + .await?; // release that failed in the fetch-step, will miss some details - env.runtime().block_on(async { - let mut conn = env.async_db().await.async_conn().await; - fake_release_that_failed_before_build( - &mut conn, - "failed_hard", - "0.1.0", - "some random error", - ) - .await - })?; + let mut conn = env.async_db().await.async_conn().await; + fake_release_that_failed_before_build( + &mut conn, + "failed_hard", + "0.1.0", + "some random error", + ) + .await?; let _m = crates_io .mock("GET", "/api/v1/crates") @@ -1370,9 +1444,10 @@ mod tests { }) .to_string(), ) - .create(); + .create_async() + .await; - let links = get_release_links("/releases/search?query=some_random_crate", web)?; + let links = get_release_links("/releases/search?query=some_random_crate", &web).await?; // `some_other_crate` won't be shown since we don't have it yet assert_eq!(links.len(), 4); @@ -1388,11 +1463,11 @@ mod tests { }) } - fn get_release_links(path: &str, web: &TestFrontend) -> Result, Error> { - let response = web.get(path).send()?; + async fn get_release_links(path: &str, web: &axum::Router) -> Result, Error> { + let response = web.get(path).await?; assert!(response.status().is_success()); - let page = kuchikiki::parse_html().one(response.text()?); + let page = kuchikiki::parse_html().one(response.text().await?); Ok(page .select("a.release") @@ -1406,36 +1481,44 @@ mod tests { #[test] fn releases_by_stars() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("crate_that_succeeded_with_github") .version("0.1.0") .github_stats("some/repo", 66, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 4, 16, 4, 33, 50).unwrap()) - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("crate_that_succeeded_with_github") .version("0.2.0") .github_stats("some/repo", 66, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 4, 20, 4, 33, 50).unwrap()) - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("crate_that_succeeded_without_github") .release_time(Utc.with_ymd_and_hms(2020, 5, 16, 4, 33, 50).unwrap()) .version("0.2.0") - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("crate_that_failed_with_github") .version("0.1.0") .github_stats("some/repo", 33, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 6, 16, 4, 33, 50).unwrap()) .build_result_failed() - .create()?; + .create_async() + .await?; - let links = get_release_links("/releases/stars", env.frontend())?; + let links = get_release_links("/releases/stars", &env.web_app().await).await?; // output is sorted by stars, not release-time assert_eq!(links.len(), 2); @@ -1451,36 +1534,44 @@ mod tests { #[test] fn failures_by_stars() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("crate_that_succeeded_with_github") .version("0.1.0") .github_stats("some/repo", 66, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 4, 16, 4, 33, 50).unwrap()) - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("crate_that_succeeded_with_github") .version("0.2.0") .github_stats("some/repo", 66, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 4, 20, 4, 33, 50).unwrap()) - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("crate_that_succeeded_without_github") .release_time(Utc.with_ymd_and_hms(2020, 5, 16, 4, 33, 50).unwrap()) .version("0.2.0") - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("crate_that_failed_with_github") .version("0.1.0") .github_stats("some/repo", 33, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 6, 16, 4, 33, 50).unwrap()) .build_result_failed() - .create()?; + .create_async() + .await?; - let links = get_release_links("/releases/failures", env.frontend())?; + let links = get_release_links("/releases/failures", &env.web_app().await).await?; // output is sorted by stars, not release-time assert_eq!(links.len(), 1); @@ -1492,28 +1583,35 @@ mod tests { #[test] fn releases_failed_by_time() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("crate_that_succeeded_with_github") .version("0.1.0") .github_stats("some/repo", 33, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 4, 16, 4, 33, 50).unwrap()) - .create()?; + .create_async() + .await?; // make sure that crates get at most one release shown, so they don't crowd the page - env.fake_release() + env.async_fake_release() + .await .name("crate_that_succeeded_with_github") .github_stats("some/repo", 33, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 5, 16, 4, 33, 50).unwrap()) .version("0.2.0") - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("crate_that_failed") .version("0.1.0") .release_time(Utc.with_ymd_and_hms(2020, 6, 16, 4, 33, 50).unwrap()) .build_result_failed() - .create()?; + .create_async() + .await?; - let links = get_release_links("/releases/recent-failures", env.frontend())?; + let links = + get_release_links("/releases/recent-failures", &env.web_app().await).await?; assert_eq!(links.len(), 1); assert_eq!(links[0], "/crate/crate_that_failed/0.1.0"); @@ -1524,36 +1622,44 @@ mod tests { #[test] fn releases_homepage_and_recent() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("crate_that_succeeded_with_github") .version("0.1.0") .github_stats("some/repo", 33, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 4, 16, 4, 33, 50).unwrap()) - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("crate_that_succeeded_with_github") .version("0.2.0-rc") .github_stats("some/repo", 33, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 4, 16, 8, 33, 50).unwrap()) .build_result_failed() - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("crate_that_succeeded_with_github") .github_stats("some/repo", 33, 22, 11) .release_time(Utc.with_ymd_and_hms(2020, 5, 16, 4, 33, 50).unwrap()) .version("0.2.0") - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("crate_that_failed") .version("0.1.0") .release_time(Utc.with_ymd_and_hms(2020, 6, 16, 4, 33, 50).unwrap()) .build_result_failed() - .create()?; + .create_async() + .await?; // make sure that crates get at most one release shown, so they don't crowd the homepage assert_eq!( - get_release_links("/", env.frontend())?, + get_release_links("/", &env.web_app().await).await?, [ "/crate/crate_that_failed/0.1.0", "/crate_that_succeeded_with_github/0.2.0/crate_that_succeeded_with_github/", @@ -1562,7 +1668,7 @@ mod tests { // but on the main release list they all show, including prerelease assert_eq!( - get_release_links("/releases", env.frontend())?, + get_release_links("/releases", &env.web_app().await).await?, [ "/crate/crate_that_failed/0.1.0", "/crate_that_succeeded_with_github/0.2.0/crate_that_succeeded_with_github/", @@ -1577,42 +1683,52 @@ mod tests { #[test] fn release_activity() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; let empty_data = format!("data: [{}]", vec!["0"; 30].join(", ")); // no data / only zeros without releases - let response = web.get("/releases/activity/").send()?; + let response = web.get("/releases/activity").await?; assert!(response.status().is_success()); - let text = response.text(); - assert_eq!(text.unwrap().matches(&empty_data).count(), 2); + let text = response.text().await?; + assert_eq!(text.matches(&empty_data).count(), 2); - env.fake_release().name("some_random_crate").create()?; - env.fake_release() + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; + env.async_fake_release() + .await .name("some_random_crate_that_failed") .build_result_failed() - .create()?; + .create_async() + .await?; // same when the release is on the current day, since we ignore today. - let response = web.get("/releases/activity/").send()?; + let response = web.get("/releases/activity").await?; assert!(response.status().is_success()); - assert_eq!(response.text().unwrap().matches(&empty_data).count(), 2); + assert_eq!(response.text().await?.matches(&empty_data).count(), 2); - env.fake_release() + env.async_fake_release() + .await .name("some_random_crate_yesterday") .release_time(Utc::now() - Duration::try_days(1).unwrap()) - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("some_random_crate_that_failed_yesterday") .build_result_failed() .release_time(Utc::now() - Duration::try_days(1).unwrap()) - .create()?; + .create_async() + .await?; // with releases yesterday we get the data we want - let response = web.get("/releases/activity/").send()?; + let response = web.get("/releases/activity").await?; assert!(response.status().is_success()); - let text = response.text().unwrap(); + let text = response.text().await?; // counts contain both releases assert!(text.contains(&format!("data: [{}, 2]", vec!["0"; 29].join(", ")))); // failures only one @@ -1624,34 +1740,40 @@ mod tests { #[test] fn release_feed() { - wrapper(|env| { - let web = env.frontend(); - assert_success("/releases/feed", web)?; + async_wrapper(|env| async move { + let web = env.web_app().await; + web.assert_success("/releases/feed").await?; - env.fake_release().name("some_random_crate").create()?; - env.fake_release() + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; + env.async_fake_release() + .await .name("some_random_crate_that_failed") .build_result_failed() - .create()?; - assert_success("/releases/feed", web) + .create_async() + .await?; + web.assert_success("/releases/feed").await?; + Ok(()) }) } #[test] fn test_deployment_queue() { - wrapper(|env| { + async_wrapper(|env| async move { env.override_config(|config| { config.cloudfront_distribution_id_web = Some("distribution_id_web".into()); }); - let web = env.frontend(); + let web = env.web_app().await; - env.runtime().block_on(async move { - let mut conn = env.async_db().await.async_conn().await; - cdn::queue_crate_invalidation(&mut conn, &env.config(), "krate_2").await - })?; + let mut conn = env.async_db().await.async_conn().await; + cdn::queue_crate_invalidation(&mut conn, &env.config(), "krate_2").await?; - let content = kuchikiki::parse_html().one(web.get("/releases/queue").send()?.text()?); + let content = + kuchikiki::parse_html().one(web.get("/releases/queue").await?.text().await?); assert!(content .select(".release > div > strong") .expect("missing heading") @@ -1673,10 +1795,11 @@ mod tests { #[test] fn test_releases_queue() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; - let empty = kuchikiki::parse_html().one(web.get("/releases/queue").send()?.text()?); + let empty = + kuchikiki::parse_html().one(web.get("/releases/queue").await?.text().await?); assert!(empty .select(".queue-list > strong") .expect("missing heading") @@ -1687,12 +1810,12 @@ mod tests { .expect("missing heading") .any(|el| el.text_contents().contains("active CDN deployments"))); - let queue = env.build_queue(); - queue.add_crate("foo", "1.0.0", 0, None)?; - queue.add_crate("bar", "0.1.0", -10, None)?; - queue.add_crate("baz", "0.0.1", 10, None)?; + let queue = env.async_build_queue().await; + queue.add_crate("foo", "1.0.0", 0, None).await?; + queue.add_crate("bar", "0.1.0", -10, None).await?; + queue.add_crate("baz", "0.0.1", 10, None).await?; - let full = kuchikiki::parse_html().one(web.get("/releases/queue").send()?.text()?); + let full = kuchikiki::parse_html().one(web.get("/releases/queue").await?.text().await?); let items = full .select(".queue-list > li") .expect("missing list items") @@ -1722,24 +1845,26 @@ mod tests { #[test] fn test_releases_queue_in_progress() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; // we have two queued releases, where the build for one is already in progress - let queue = env.build_queue(); - queue.add_crate("foo", "1.0.0", 0, None)?; - queue.add_crate("bar", "0.1.0", 0, None)?; + let queue = env.async_build_queue().await; + queue.add_crate("foo", "1.0.0", 0, None).await?; + queue.add_crate("bar", "0.1.0", 0, None).await?; - env.fake_release() + env.async_fake_release() + .await .name("foo") .version("1.0.0") .builds(vec![FakeBuild::default() .build_status(BuildStatus::InProgress) .rustc_version("rustc (blabla 2022-01-01)") .docsrs_version("docs.rs 4.0.0")]) - .create()?; + .create_async() + .await?; - let full = kuchikiki::parse_html().one(web.get("/releases/queue").send()?.text()?); + let full = kuchikiki::parse_html().one(web.get("/releases/queue").await?.text().await?); let lists = full .select(".queue-list") @@ -1769,10 +1894,11 @@ mod tests { #[test] fn test_releases_rebuild_queue_empty() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; - let empty = kuchikiki::parse_html().one(web.get("/releases/queue").send()?.text()?); + let empty = + kuchikiki::parse_html().one(web.get("/releases/queue").await?.text().await?); assert!(empty .select(".about > p") @@ -1790,14 +1916,20 @@ mod tests { #[test] fn test_releases_rebuild_queue_with_crates() { - wrapper(|env| { - let web = env.frontend(); - let queue = env.build_queue(); - queue.add_crate("foo", "1.0.0", REBUILD_PRIORITY, None)?; - queue.add_crate("bar", "0.1.0", REBUILD_PRIORITY + 1, None)?; - queue.add_crate("baz", "0.0.1", REBUILD_PRIORITY - 1, None)?; - - let full = kuchikiki::parse_html().one(web.get("/releases/queue").send()?.text()?); + async_wrapper(|env| async move { + let web = env.web_app().await; + let queue = env.async_build_queue().await; + queue + .add_crate("foo", "1.0.0", REBUILD_PRIORITY, None) + .await?; + queue + .add_crate("bar", "0.1.0", REBUILD_PRIORITY + 1, None) + .await?; + queue + .add_crate("baz", "0.0.1", REBUILD_PRIORITY - 1, None) + .await?; + + let full = kuchikiki::parse_html().one(web.get("/releases/queue").await?.text().await?); let items = full .select(".rebuild-queue-list > li") .expect("missing list items") @@ -1812,8 +1944,8 @@ mod tests { .text_contents() .contains("There are currently 2 crates in the rebuild queue"))); - let full = - kuchikiki::parse_html().one(web.get("/releases/queue?expand=1").send()?.text()?); + let full = kuchikiki::parse_html() + .one(web.get("/releases/queue?expand=1").await?.text().await?); let build_queue_list = full .select(".queue-list > li") .expect("missing list items") @@ -1844,27 +1976,29 @@ mod tests { #[test] fn home_page_links() { - wrapper(|env| { - let web = env.frontend(); - env.fake_release() + async_wrapper(|env| async move { + let web = env.web_app().await; + env.async_fake_release() + .await .name("some_random_crate") .add_owner(CrateOwner { login: "foobar".into(), avatar: "https://example.org/foobar".into(), kind: OwnerKind::User, }) - .create()?; + .create_async() + .await?; let mut urls = vec![]; let mut seen = HashSet::new(); seen.insert("".to_owned()); - let resp = web.get("").send()?; - assert_cache_control(&resp, CachePolicy::ShortInCdnAndBrowser, &env.config()); + let resp = web.get("/").await?; + resp.assert_cache_control(CachePolicy::ShortInCdnAndBrowser, &env.config()); assert!(resp.status().is_success()); - let html = kuchikiki::parse_html().one(resp.text()?); + let html = kuchikiki::parse_html().one(resp.text().await?); for link in html.select("a").unwrap() { let link = link.as_node().as_element().unwrap(); @@ -1882,7 +2016,7 @@ mod tests { // Skip external links continue; } else { - web.get(&url).send()? + web.get(&url).await? }; let status = resp.status(); assert!(status.is_success(), "failed to GET {url}: {status}"); @@ -1896,10 +2030,17 @@ mod tests { fn check_releases_page_content() { // NOTE: this is a little fragile and may have to be updated if the HTML layout changes let sel = ".pure-menu-horizontal>.pure-menu-list>.pure-menu-item>.pure-menu-link>.title"; - wrapper(|env| { - let tester = |url| { + async_wrapper(|env| async move { + for url in &[ + "/releases", + "/releases/stars", + "/releases/recent-failures", + "/releases/failures", + "/releases/activity", + "/releases/queue", + ] { let page = kuchikiki::parse_html() - .one(env.frontend().get(url).send().unwrap().text().unwrap()); + .one(env.web_app().await.get(url).await.unwrap().text().await?); assert_eq!(page.select("#crate-title").unwrap().count(), 1); let not_matching = page .select(sel) @@ -1926,17 +2067,6 @@ mod tests { "Titles did not match for URL `{url}`: not found: {not_found:?}, found: {found:?}", ); } - }; - - for url in &[ - "/releases", - "/releases/stars", - "/releases/recent-failures", - "/releases/failures", - "/releases/activity", - "/releases/queue", - ] { - tester(url); } Ok(()) @@ -1945,10 +2075,11 @@ mod tests { #[test] fn check_owner_releases_redirect() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; - assert_redirect_unchecked("/releases/someone", "https://crates.io/users/someone", web)?; + web.assert_redirect_unchecked("/releases/someone", "https://crates.io/users/someone") + .await?; Ok(()) }); } diff --git a/src/web/routes.rs b/src/web/routes.rs index 7b2517d13..73d9a5b17 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -368,26 +368,26 @@ async fn fallback() -> impl IntoResponse { #[cfg(test)] mod tests { - use crate::test::*; + use crate::test::{async_wrapper, AxumResponseTestExt, AxumRouterTestExt}; use crate::web::cache::CachePolicy; use reqwest::StatusCode; #[test] fn test_root_redirects() { - wrapper(|env| { + async_wrapper(|env| async move { + let web = env.web_app().await; // These are "well-known" resources that will be requested from the root, but support // redirection - assert_redirect("/favicon.ico", "/-/static/favicon.ico", env.frontend())?; - assert_redirect("/robots.txt", "/-/static/robots.txt", env.frontend())?; + web.assert_redirect("/favicon.ico", "/-/static/favicon.ico") + .await?; + web.assert_redirect("/robots.txt", "/-/static/robots.txt") + .await?; // This has previously been served with a url pointing to the root, it may be // plausible to remove the redirects in the future, but for now we need to keep serving // it. - assert_redirect( - "/opensearch.xml", - "/-/static/opensearch.xml", - env.frontend(), - )?; + web.assert_redirect("/opensearch.xml", "/-/static/opensearch.xml") + .await?; Ok(()) }); @@ -395,35 +395,38 @@ mod tests { #[test] fn serve_rustdoc_content_not_found() { - wrapper(|env| { - let response = env.frontend().get("/-/rustdoc.static/style.css").send()?; + async_wrapper(|env| async move { + let response = env + .web_app() + .await + .get("/-/rustdoc.static/style.css") + .await?; assert_eq!(response.status(), StatusCode::NOT_FOUND); - assert_cache_control(&response, CachePolicy::NoCaching, &env.config()); + response.assert_cache_control(CachePolicy::NoCaching, &env.config()); Ok(()) }) } #[test] fn serve_rustdoc_content() { - wrapper(|env| { - let web = env.frontend(); - env.storage() - .store_one("/rustdoc-static/style.css", "content".as_bytes())?; - env.storage() - .store_one("/will_not/be_found.css", "something".as_bytes())?; + async_wrapper(|env| async move { + let web = env.web_app().await; + let storage = env.async_storage().await; + storage + .store_one("/rustdoc-static/style.css", "content".as_bytes()) + .await?; + storage + .store_one("/will_not/be_found.css", "something".as_bytes()) + .await?; - let response = web.get("/-/rustdoc.static/style.css").send()?; + let response = web.get("/-/rustdoc.static/style.css").await?; assert!(response.status().is_success()); - assert_cache_control( - &response, - CachePolicy::ForeverInCdnAndBrowser, - &env.config(), - ); - assert_eq!(response.text()?, "content"); + response.assert_cache_control(CachePolicy::ForeverInCdnAndBrowser, &env.config()); + assert_eq!(response.text().await?, "content"); assert_eq!( web.get("/-/rustdoc.static/will_not/be_found.css") - .send()? + .await? .status(), StatusCode::NOT_FOUND ); diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index 002380256..8e6e94219 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -859,29 +859,25 @@ mod test { registry_api::{CrateOwner, OwnerKind}, test::*, utils::Dependency, - web::cache::CachePolicy, + web::{cache::CachePolicy, encode_url_path}, Config, }; use anyhow::Context; use kuchikiki::traits::TendrilSink; - use reqwest::{blocking::ClientBuilder, redirect, StatusCode}; + use reqwest::StatusCode; use std::collections::BTreeMap; use test_case::test_case; use tracing::info; - fn try_latest_version_redirect( + async fn try_latest_version_redirect( path: &str, - web: &TestFrontend, + web: &axum::Router, config: &Config, ) -> Result, anyhow::Error> { - assert_success(path, web)?; - let response = web.get(path).send()?; - assert_cache_control( - &response, - CachePolicy::ForeverInCdnAndStaleInBrowser, - config, - ); - let data = response.text()?; + web.assert_success(path).await?; + let response = web.get(path).await?; + response.assert_cache_control(CachePolicy::ForeverInCdnAndStaleInBrowser, config); + let data = response.text().await?; info!("fetched path {} and got content {}\nhelp: if this is missing the header, remember to add ", path, data); let dom = kuchikiki::parse_html().one(data); @@ -891,19 +887,22 @@ mod test { .next() { let link = elem.attributes.borrow().get("href").unwrap().to_string(); - assert_success_cached(&link, web, CachePolicy::ForeverInCdn, config)?; + let response = web.get(&link).await?; + response.assert_cache_control(CachePolicy::ForeverInCdn, config); + assert!(response.status().is_success() || response.status().is_redirection()); Ok(Some(link)) } else { Ok(None) } } - fn latest_version_redirect( + async fn latest_version_redirect( path: &str, - web: &TestFrontend, + web: &axum::Router, config: &Config, ) -> Result { - try_latest_version_redirect(path, web, config)? + try_latest_version_redirect(path, web, config) + .await? .with_context(|| anyhow::anyhow!("no redirect found for {}", path)) } @@ -911,20 +910,22 @@ mod test { #[test_case(false)] // https://github.com/rust-lang/docs.rs/issues/2313 fn help_html(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("krate") .version("0.1.0") .archive_storage(archive_storage) .rustdoc_file("help.html") - .create()?; - let web = env.frontend(); - assert_success_cached( + .create_async() + .await?; + let web = env.web_app().await; + web.assert_success_cached( "/krate/0.1.0/help.html", - web, CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config(), - )?; + ) + .await?; Ok(()) }); } @@ -933,9 +934,10 @@ mod test { #[test_case(false)] // regression test for https://github.com/rust-lang/docs.rs/issues/552 fn settings_html(archive_storage: bool) { - wrapper(|env| { + async_wrapper(|env| async move { // first release works, second fails - env.fake_release() + env.async_fake_release() + .await .name("buggy") .version("0.1.0") .archive_storage(archive_storage) @@ -946,63 +948,67 @@ mod test { .rustdoc_file("all.html") .rustdoc_file("directory_3/.gitignore") .rustdoc_file("directory_4/empty_file_no_ext") - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("buggy") .version("0.2.0") .archive_storage(archive_storage) .build_result_failed() - .create()?; - let web = env.frontend(); - assert_success_cached("/", web, CachePolicy::ShortInCdnAndBrowser, &env.config())?; - assert_success_cached( - "/crate/buggy/0.1.0/", - web, + .create_async() + .await?; + let web = env.web_app().await; + web.assert_success_cached("/", CachePolicy::ShortInCdnAndBrowser, &env.config()) + .await?; + web.assert_success_cached( + "/crate/buggy/0.1.0", CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config(), - )?; - assert_success_cached( + ) + .await?; + web.assert_success_cached( "/buggy/0.1.0/directory_1/index.html", - web, CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config(), - )?; - assert_success_cached( + ) + .await?; + web.assert_success_cached( "/buggy/0.1.0/directory_2.html/index.html", - web, CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config(), - )?; - assert_success_cached( + ) + .await?; + web.assert_success_cached( "/buggy/0.1.0/directory_3/.gitignore", - web, CachePolicy::ForeverInCdnAndBrowser, &env.config(), - )?; - assert_success_cached( + ) + .await?; + web.assert_success_cached( "/buggy/0.1.0/settings.html", - web, CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config(), - )?; - assert_success_cached( + ) + .await?; + web.assert_success_cached( "/buggy/0.1.0/scrape-examples-help.html", - web, CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config(), - )?; - assert_success_cached( + ) + .await?; + web.assert_success_cached( "/buggy/0.1.0/all.html", - web, CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config(), - )?; - assert_success_cached( + ) + .await?; + web.assert_success_cached( "/buggy/0.1.0/directory_4/empty_file_no_ext", - web, CachePolicy::ForeverInCdnAndBrowser, &env.config(), - )?; + ) + .await?; Ok(()) }); } @@ -1010,89 +1016,99 @@ mod test { #[test_case(true)] #[test_case(false)] fn default_target_redirects_to_base(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(archive_storage) .rustdoc_file("dummy/index.html") - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; // no explicit default-target let base = "/dummy/0.1.0/dummy/"; - assert_success_cached( + web.assert_success_cached( base, - web, CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config(), - )?; - assert_redirect_cached( + ) + .await?; + web.assert_redirect_cached( "/dummy/0.1.0/x86_64-unknown-linux-gnu/dummy/", base, CachePolicy::ForeverInCdn, - web, &env.config(), - )?; + ) + .await?; - assert_success("/dummy/latest/dummy/", web)?; + web.assert_success("/dummy/latest/dummy/").await?; // set an explicit target that requires cross-compile let target = "x86_64-pc-windows-msvc"; - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.2.0") .archive_storage(archive_storage) .rustdoc_file("dummy/index.html") .default_target(target) - .create()?; + .create_async() + .await?; let base = "/dummy/0.2.0/dummy/"; - assert_success(base, web)?; - assert_redirect("/dummy/0.2.0/x86_64-pc-windows-msvc/dummy/", base, web)?; + web.assert_success(base).await?; + web.assert_redirect("/dummy/0.2.0/x86_64-pc-windows-msvc/dummy/", base) + .await?; // set an explicit target without cross-compile // also check that /:crate/:version/:platform/all.html doesn't panic let target = "x86_64-unknown-linux-gnu"; - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.3.0") .archive_storage(archive_storage) .rustdoc_file("dummy/index.html") .rustdoc_file("all.html") .default_target(target) - .create()?; + .create_async() + .await?; let base = "/dummy/0.3.0/dummy/"; - assert_success(base, web)?; - assert_redirect("/dummy/0.3.0/x86_64-unknown-linux-gnu/dummy/", base, web)?; - assert_redirect( + web.assert_success(base).await?; + web.assert_redirect("/dummy/0.3.0/x86_64-unknown-linux-gnu/dummy/", base) + .await?; + web.assert_redirect( "/dummy/0.3.0/x86_64-unknown-linux-gnu/all.html", "/dummy/0.3.0/all.html", - web, - )?; - assert_redirect("/dummy/0.3.0/", base, web)?; - assert_redirect("/dummy/0.3.0/index.html", base, web)?; + ) + .await?; + web.assert_redirect("/dummy/0.3.0/", base).await?; + web.assert_redirect("/dummy/0.3.0/index.html", base).await?; Ok(()) }); } #[test] fn latest_url() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(true) .rustdoc_file("dummy/index.html") - .create()?; + .create_async() + .await?; let resp = env - .frontend() + .web_app() + .await .get("/dummy/latest/dummy/") - .send()? + .await? .error_for_status()?; - assert_cache_control(&resp, CachePolicy::ForeverInCdn, &env.config()); - assert!(resp.url().as_str().ends_with("/dummy/latest/dummy/")); - let body = String::from_utf8(resp.bytes().unwrap().to_vec()).unwrap(); + resp.assert_cache_control(CachePolicy::ForeverInCdn, &env.config()); + let body = resp.text().await?; assert!(body.contains(" Result { - assert_success(path, web)?; - let data = web.get(path).send()?.text()?; + async fn has_yanked_warning(path: &str, web: &axum::Router) -> Result { + web.assert_success(path).await?; + let data = web.get(path).await?.text().await?; Ok(kuchikiki::parse_html() .one(data) .select("form > ul > li > .warn") @@ -1320,28 +1365,32 @@ mod test { .any(|el| el.text_contents().contains("yanked"))) } - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(archive_storage) .rustdoc_file("dummy/index.html") .yanked(true) - .create()?; + .create_async() + .await?; - assert!(has_yanked_warning("/dummy/0.1.0/dummy/", web)?); + assert!(has_yanked_warning("/dummy/0.1.0/dummy/", &web).await?); - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.2.0") .archive_storage(archive_storage) .rustdoc_file("dummy/index.html") .yanked(true) - .create()?; + .create_async() + .await?; - assert!(has_yanked_warning("/dummy/0.1.0/dummy/", web)?); + assert!(has_yanked_warning("/dummy/0.1.0/dummy/", &web).await?); Ok(()) }) @@ -1349,48 +1398,24 @@ mod test { #[test] fn badges_are_urlencoded() { - wrapper(|env| { - use reqwest::Url; - use url::Host; - - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("zstd") .version("0.5.1+zstd.1.4.4") - .create()?; - - let frontend = env.override_frontend(|frontend| { - use reqwest::blocking::Client; - use reqwest::redirect::Policy; - // avoid making network requests - frontend.client = Client::builder().redirect(Policy::none()).build().unwrap(); - }); - let mut last_url = "/zstd/badge.svg".to_owned(); - let mut response = frontend.get(&last_url).send()?; - let mut current_url = response.url().clone(); - // follow redirects until it actually goes out into the internet - while !matches!(current_url.host(), Some(Host::Domain(_))) { - println!("({last_url} -> {current_url})"); - assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY); - last_url = response.url().to_string(); - response = frontend.get(response.url().as_str()).send().unwrap(); - current_url = Url::parse(response.headers()[reqwest::header::LOCATION].to_str()?)?; - } - println!("({last_url} -> {current_url})"); + .create_async() + .await?; + + let frontend = env.web_app().await; + let response = frontend + .assert_redirect_cached_unchecked( + "/zstd/badge.svg", + "https://img.shields.io/docsrs/zstd/latest", + CachePolicy::ForeverInCdnAndBrowser, + &env.config(), + ) + .await?; assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY); - assert_cache_control( - &response, - CachePolicy::ForeverInCdnAndBrowser, - &env.config(), - ); - assert_eq!( - current_url.as_str(), - "https://img.shields.io/docsrs/zstd/latest" - ); - // make sure we aren't actually making network requests - assert_ne!( - response.url().as_str(), - "https://img.shields.io/docsrs/zstd/latest" - ); Ok(()) }) @@ -1399,16 +1424,19 @@ mod test { #[test_case(true)] #[test_case(false)] fn crate_name_percent_decoded_redirect(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("fake-crate") .version("0.0.1") .archive_storage(archive_storage) .rustdoc_file("fake_crate/index.html") - .create()?; + .create_async() + .await?; - let web = env.frontend(); - assert_redirect("/fake%2Dcrate", "/fake-crate/latest/fake_crate/", web)?; + let web = env.web_app().await; + web.assert_redirect("/fake%2Dcrate", "/fake-crate/latest/fake_crate/") + .await?; Ok(()) }); @@ -1417,7 +1445,7 @@ mod test { #[test_case(true)] #[test_case(false)] fn base_redirect_handles_mismatched_separators(archive_storage: bool) { - wrapper(|env| { + async_wrapper(|env| async move { let rels = [ ("dummy-dash", "0.1.0"), ("dummy-dash", "0.2.0"), @@ -1428,49 +1456,54 @@ mod test { ]; for (name, version) in &rels { - env.fake_release() + env.async_fake_release() + .await .name(name) .version(version) .archive_storage(archive_storage) .rustdoc_file(&(name.replace('-', "_") + "/index.html")) - .create()?; + .create_async() + .await?; } - let web = env.frontend(); + let web = env.web_app().await; - assert_redirect("/dummy_dash", "/dummy-dash/latest/dummy_dash/", web)?; - assert_redirect("/dummy_dash/*", "/dummy-dash/latest/dummy_dash/", web)?; - assert_redirect("/dummy_dash/0.1.0", "/dummy-dash/0.1.0/dummy_dash/", web)?; - assert_redirect( + web.assert_redirect("/dummy_dash", "/dummy-dash/latest/dummy_dash/") + .await?; + web.assert_redirect("/dummy_dash/*", "/dummy-dash/latest/dummy_dash/") + .await?; + web.assert_redirect("/dummy_dash/0.1.0", "/dummy-dash/0.1.0/dummy_dash/") + .await?; + web.assert_redirect( "/dummy-underscore", "/dummy_underscore/latest/dummy_underscore/", - web, - )?; - assert_redirect( + ) + .await?; + web.assert_redirect( "/dummy-underscore/*", "/dummy_underscore/latest/dummy_underscore/", - web, - )?; - assert_redirect( + ) + .await?; + web.assert_redirect( "/dummy-underscore/0.1.0", "/dummy_underscore/0.1.0/dummy_underscore/", - web, - )?; - assert_redirect( + ) + .await?; + web.assert_redirect( "/dummy-mixed_separators", "/dummy_mixed-separators/latest/dummy_mixed_separators/", - web, - )?; - assert_redirect( + ) + .await?; + web.assert_redirect( "/dummy_mixed_separators/*", "/dummy_mixed-separators/latest/dummy_mixed_separators/", - web, - )?; - assert_redirect( + ) + .await?; + web.assert_redirect( "/dummy-mixed-separators/0.1.0", "/dummy_mixed-separators/0.1.0/dummy_mixed_separators/", - web, - )?; + ) + .await?; Ok(()) }) @@ -1479,34 +1512,43 @@ mod test { #[test_case(true)] #[test_case(false)] fn specific_pages_do_not_handle_mismatched_separators(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy-dash") .version("0.1.0") .archive_storage(archive_storage) .rustdoc_file("dummy_dash/index.html") - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("dummy_mixed-separators") .version("0.1.0") .archive_storage(archive_storage) .rustdoc_file("dummy_mixed_separators/index.html") - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; - assert_success("/dummy-dash/0.1.0/dummy_dash/index.html", web)?; - assert_success("/crate/dummy_mixed-separators", web)?; + web.assert_success("/dummy-dash/0.1.0/dummy_dash/index.html") + .await?; + web.assert_redirect_unchecked( + "/crate/dummy_mixed-separators", + "/crate/dummy_mixed-separators/latest", + ) + .await?; - assert_redirect( + web.assert_redirect( "/dummy_dash/0.1.0/dummy_dash/index.html", "/dummy-dash/0.1.0/dummy_dash/index.html", - web, - )?; + ) + .await?; assert_eq!( - web.get("/crate/dummy_mixed_separators").send()?.status(), + dbg!(web.get("/crate/dummy_mixed_separators/latest").await?).status(), StatusCode::NOT_FOUND ); @@ -1516,9 +1558,9 @@ mod test { #[test] fn nonexistent_crate_404s() { - wrapper(|env| { + async_wrapper(|env| async move { assert_eq!( - env.frontend().get("/dummy").send()?.status(), + env.web_app().await.get("/dummy").await?.status(), StatusCode::NOT_FOUND ); @@ -1528,19 +1570,21 @@ mod test { #[test] fn no_target_target_redirect_404s() { - wrapper(|env| { + async_wrapper(|env| async move { assert_eq!( - env.frontend() + env.web_app() + .await .get("/crate/dummy/0.1.0/target-redirect") - .send()? + .await? .status(), StatusCode::NOT_FOUND ); assert_eq!( - env.frontend() + env.web_app() + .await .get("/crate/dummy/0.1.0/target-redirect/") - .send()? + .await? .status(), StatusCode::NOT_FOUND ); @@ -1552,12 +1596,12 @@ mod test { #[test_case(true)] #[test_case(false)] fn platform_links_go_to_current_path(archive_storage: bool) { - fn get_platform_links( + async fn get_platform_links( path: &str, - web: &TestFrontend, + web: &axum::Router, ) -> Result, anyhow::Error> { - assert_success(path, web)?; - let data = web.get(path).send()?.text()?; + web.assert_success(path).await?; + let data = web.get(path).await?.text().await?; let dom = kuchikiki::parse_html().one(data); Ok(dom .select(r#"a[aria-label="Platform"] + ul li a"#) @@ -1571,16 +1615,17 @@ mod test { }) .collect()) } - fn assert_platform_links( - web: &TestFrontend, + async fn assert_platform_links( + web: &axum::Router, path: &str, links: &[(&str, &str)], ) -> Result<(), anyhow::Error> { let mut links: BTreeMap<_, _> = links.iter().copied().collect(); - for (platform, link, rel) in get_platform_links(path, web)? { + for (platform, link, rel) in get_platform_links(path, web).await? { assert_eq!(rel, "nofollow"); - assert_redirect(&link, links.remove(platform.as_str()).unwrap(), web)?; + web.assert_redirect(&link, links.remove(platform.as_str()).unwrap()) + .await?; } assert!(links.is_empty()); @@ -1588,167 +1633,192 @@ mod test { Ok(()) } - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; // no explicit default-target - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(archive_storage) .rustdoc_file("dummy/index.html") .rustdoc_file("dummy/struct.Dummy.html") .add_target("x86_64-unknown-linux-gnu") - .create()?; + .create_async() + .await?; assert_platform_links( - web, + &web, "/dummy/0.1.0/dummy/", &[("x86_64-unknown-linux-gnu", "/dummy/0.1.0/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.1.0/dummy/index.html", &[("x86_64-unknown-linux-gnu", "/dummy/0.1.0/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.1.0/dummy/struct.Dummy.html", &[( "x86_64-unknown-linux-gnu", "/dummy/0.1.0/dummy/struct.Dummy.html", )], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/dummy/", &[("x86_64-unknown-linux-gnu", "/dummy/latest/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/dummy/index.html", &[("x86_64-unknown-linux-gnu", "/dummy/latest/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/dummy/struct.Dummy.html", &[( "x86_64-unknown-linux-gnu", "/dummy/latest/dummy/struct.Dummy.html", )], - )?; + ) + .await?; // set an explicit target that requires cross-compile - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.2.0") .archive_storage(archive_storage) .rustdoc_file("dummy/index.html") .rustdoc_file("dummy/struct.Dummy.html") .default_target("x86_64-pc-windows-msvc") - .create()?; + .create_async() + .await?; assert_platform_links( - web, + &web, "/dummy/0.2.0/dummy/", &[("x86_64-pc-windows-msvc", "/dummy/0.2.0/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.2.0/dummy/index.html", &[("x86_64-pc-windows-msvc", "/dummy/0.2.0/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.2.0/dummy/struct.Dummy.html", &[( "x86_64-pc-windows-msvc", "/dummy/0.2.0/dummy/struct.Dummy.html", )], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/dummy/", &[("x86_64-pc-windows-msvc", "/dummy/latest/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/dummy/index.html", &[("x86_64-pc-windows-msvc", "/dummy/latest/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/dummy/struct.Dummy.html", &[( "x86_64-pc-windows-msvc", "/dummy/latest/dummy/struct.Dummy.html", )], - )?; + ) + .await?; // set an explicit target without cross-compile - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.3.0") .archive_storage(archive_storage) .rustdoc_file("dummy/index.html") .rustdoc_file("dummy/struct.Dummy.html") .default_target("x86_64-unknown-linux-gnu") - .create()?; + .create_async() + .await?; assert_platform_links( - web, + &web, "/dummy/0.3.0/dummy/", &[("x86_64-unknown-linux-gnu", "/dummy/0.3.0/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.3.0/dummy/index.html", &[("x86_64-unknown-linux-gnu", "/dummy/0.3.0/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.3.0/dummy/struct.Dummy.html", &[( "x86_64-unknown-linux-gnu", "/dummy/0.3.0/dummy/struct.Dummy.html", )], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/dummy/", &[("x86_64-unknown-linux-gnu", "/dummy/latest/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/dummy/index.html", &[("x86_64-unknown-linux-gnu", "/dummy/latest/dummy/index.html")], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/dummy/struct.Dummy.html", &[( "x86_64-unknown-linux-gnu", "/dummy/latest/dummy/struct.Dummy.html", )], - )?; + ) + .await?; // multiple targets - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.4.0") .archive_storage(archive_storage) @@ -1762,10 +1832,11 @@ mod test { .rustdoc_file("x86_64-pc-windows-msvc/dummy/struct.WindowsOnly.html") .default_target("x86_64-unknown-linux-gnu") .add_target("x86_64-pc-windows-msvc") - .create()?; + .create_async() + .await?; assert_platform_links( - web, + &web, "/dummy/0.4.0/settings.html", &[ ( @@ -1774,10 +1845,11 @@ mod test { ), ("x86_64-unknown-linux-gnu", "/dummy/0.4.0/settings.html"), ], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/latest/settings.html", &[ ( @@ -1786,10 +1858,11 @@ mod test { ), ("x86_64-unknown-linux-gnu", "/dummy/latest/settings.html"), ], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.4.0/dummy/", &[ ( @@ -1798,10 +1871,11 @@ mod test { ), ("x86_64-unknown-linux-gnu", "/dummy/0.4.0/dummy/index.html"), ], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.4.0/x86_64-pc-windows-msvc/dummy/index.html", &[ ( @@ -1810,10 +1884,11 @@ mod test { ), ("x86_64-unknown-linux-gnu", "/dummy/0.4.0/dummy/index.html"), ], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.4.0/dummy/index.html", &[ ( @@ -1822,10 +1897,11 @@ mod test { ), ("x86_64-unknown-linux-gnu", "/dummy/0.4.0/dummy/index.html"), ], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.4.0/dummy/struct.DefaultOnly.html", &[ ( @@ -1837,10 +1913,11 @@ mod test { "/dummy/0.4.0/dummy/struct.DefaultOnly.html", ), ], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.4.0/dummy/struct.Dummy.html", &[ ( @@ -1852,10 +1929,11 @@ mod test { "/dummy/0.4.0/dummy/struct.Dummy.html", ), ], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.4.0/x86_64-pc-windows-msvc/dummy/struct.Dummy.html", &[ ( @@ -1867,10 +1945,11 @@ mod test { "/dummy/0.4.0/dummy/struct.Dummy.html", ), ], - )?; + ) + .await?; assert_platform_links( - web, + &web, "/dummy/0.4.0/x86_64-pc-windows-msvc/dummy/struct.WindowsOnly.html", &[ ( @@ -1882,7 +1961,8 @@ mod test { "/dummy/0.4.0/dummy/?search=WindowsOnly", ), ], - )?; + ) + .await?; Ok(()) }); @@ -1890,30 +1970,32 @@ mod test { #[test] fn test_target_redirect_with_corrected_name() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo_ab") .version("0.0.1") .archive_storage(true) - .create()?; + .create_async() + .await?; - let web = env.frontend(); - assert_redirect_unchecked( + let web = env.web_app().await; + web.assert_redirect_unchecked( "/crate/foo-ab/0.0.1/target-redirect/x86_64-unknown-linux-gnu", "/foo-ab/0.0.1/foo_ab/", - web, - )?; + ) + .await?; Ok(()) }) } #[test] fn test_target_redirect_not_found() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; assert_eq!( web.get("/crate/fdsafdsafdsafdsa/0.1.0/target-redirect/x86_64-apple-darwin/") - .send()? + .await? .status(), StatusCode::NOT_FOUND, ); @@ -1923,24 +2005,19 @@ mod test { #[test] fn test_redirect_to_latest_302() { - wrapper(|env| { - env.fake_release().name("dummy").version("1.0.0").create()?; - let web = env.frontend(); - let client = ClientBuilder::new() - .redirect(redirect::Policy::none()) - .build() - .unwrap(); - let url = format!("http://{}/dummy", web.server_addr()); - let resp = client.get(url).send()?; + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("dummy") + .version("1.0.0") + .create_async() + .await?; + let web = env.web_app().await; + let resp = web + .assert_redirect("/dummy", "/dummy/latest/dummy/") + .await?; assert_eq!(resp.status(), StatusCode::FOUND); assert!(resp.headers().get("Cache-Control").is_none()); - assert!(resp - .headers() - .get("Location") - .unwrap() - .to_str() - .unwrap() - .contains("/dummy/latest/dummy/")); Ok(()) }) } @@ -1948,21 +2025,27 @@ mod test { #[test_case(true)] #[test_case(false)] fn test_fully_yanked_crate_404s(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("1.0.0") .archive_storage(archive_storage) .yanked(true) - .create()?; + .create_async() + .await?; assert_eq!( - env.frontend().get("/crate/dummy").send()?.status(), + env.web_app() + .await + .get("/crate/dummy/latest") + .await? + .status(), StatusCode::NOT_FOUND ); assert_eq!( - env.frontend().get("/dummy").send()?.status(), + env.web_app().await.get("/dummy/").await?.status(), StatusCode::NOT_FOUND ); @@ -1974,66 +2057,77 @@ mod test { #[test_case(false)] fn test_no_trailing_target_slash(archive_storage: bool) { // regression test for https://github.com/rust-lang/docs.rs/issues/856 - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(archive_storage) - .create()?; - let web = env.frontend(); - assert_redirect( + .create_async() + .await?; + let web = env.web_app().await; + web.assert_redirect( "/crate/dummy/0.1.0/target-redirect/x86_64-apple-darwin", "/dummy/0.1.0/dummy/", - web, - )?; - env.fake_release() + ) + .await?; + env.async_fake_release() + .await .name("dummy") .version("0.2.0") .archive_storage(archive_storage) .add_platform("x86_64-apple-darwin") - .create()?; - assert_redirect( + .create_async() + .await?; + web.assert_redirect( "/crate/dummy/0.2.0/target-redirect/x86_64-apple-darwin", "/dummy/0.2.0/x86_64-apple-darwin/dummy/", - web, - )?; - assert_redirect( + ) + .await?; + web.assert_redirect( "/crate/dummy/0.2.0/target-redirect/platform-that-does-not-exist", "/dummy/0.2.0/dummy/", - web, - )?; + ) + .await?; Ok(()) }) } #[test] fn test_redirect_crate_coloncolon_path() { - wrapper(|env| { - let web = env.frontend(); - env.fake_release().name("some_random_crate").create()?; - env.fake_release().name("some_other_crate").create()?; - - assert_redirect( + async_wrapper(|env| async move { + let web = env.web_app().await; + env.async_fake_release() + .await + .name("some_random_crate") + .create_async() + .await?; + env.async_fake_release() + .await + .name("some_other_crate") + .create_async() + .await?; + + web.assert_redirect( "/some_random_crate::somepath", "/some_random_crate/latest/some_random_crate/?search=somepath", - web, - )?; - assert_redirect( + ) + .await?; + web.assert_redirect( "/some_random_crate::some::path", "/some_random_crate/latest/some_random_crate/?search=some%3A%3Apath", - web, - )?; - assert_redirect( + ) + .await?; + web.assert_redirect( "/some_random_crate::some::path?go_to_first=true", "/some_random_crate/latest/some_random_crate/?go_to_first=true&search=some%3A%3Apath", - web, - )?; + ).await?; - assert_redirect( + web.assert_redirect_unchecked( "/std::some::path", "https://doc.rust-lang.org/stable/std/?search=some%3A%3Apath", - web, - )?; + ) + .await?; Ok(()) }) @@ -2042,25 +2136,24 @@ mod test { #[test] // regression test for https://github.com/rust-lang/docs.rs/pull/885#issuecomment-655147643 fn test_no_panic_on_missing_kind() { - wrapper(|env| { + async_wrapper(|env| async move { let id = env - .fake_release() + .async_fake_release() + .await .name("strum") .version("0.13.0") - .create()?; - - env.runtime().block_on(async { - let mut conn = env.async_db().await.async_conn().await; - // https://stackoverflow.com/questions/18209625/how-do-i-modify-fields-inside-the-new-postgresql-json-datatype - sqlx::query!( - r#"UPDATE releases SET dependencies = dependencies::jsonb #- '{0,2}' WHERE id = $1"#, id.0) - .execute(&mut *conn) - .await - })?; - - let web = env.frontend(); - assert_success("/strum/0.13.0/strum/", web)?; - assert_success("/crate/strum/0.13.0/", web)?; + .create_async() + .await?; + + let mut conn = env.async_db().await.async_conn().await; + // https://stackoverflow.com/questions/18209625/how-do-i-modify-fields-inside-the-new-postgresql-json-datatype + sqlx::query!( + r#"UPDATE releases SET dependencies = dependencies::jsonb #- '{0,2}' WHERE id = $1"#, id.0 + ).execute(&mut *conn).await?; + + let web = env.web_app().await; + web.assert_success("/strum/0.13.0/strum/").await?; + web.assert_success("/crate/strum/0.13.0").await?; Ok(()) }) } @@ -2068,15 +2161,23 @@ mod test { #[test] // regression test for https://github.com/rust-lang/docs.rs/pull/885#issuecomment-655154405 fn test_readme_rendered_as_html() { - wrapper(|env| { + async_wrapper(|env| async move { let readme = "# Overview"; - env.fake_release() + env.async_fake_release() + .await .name("strum") .version("0.18.0") .readme(readme) - .create()?; - let page = kuchikiki::parse_html() - .one(env.frontend().get("/crate/strum/0.18.0").send()?.text()?); + .create_async() + .await?; + let page = kuchikiki::parse_html().one( + env.web_app() + .await + .get("/crate/strum/0.18.0") + .await? + .text() + .await?, + ); let rendered = page.select_first("#main").expect("missing readme"); println!("{}", rendered.text_contents()); rendered @@ -2090,34 +2191,41 @@ mod test { #[test] // regression test for https://github.com/rust-lang/docs.rs/pull/885#issuecomment-655149288 fn test_build_status_is_accurate() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("hexponent") .version("0.3.0") - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("hexponent") .version("0.2.0") .build_result_failed() - .create()?; - let web = env.frontend(); - - let status = |version| -> Result<_, anyhow::Error> { - let page = - kuchikiki::parse_html().one(web.get("/crate/hexponent/0.3.0").send()?.text()?); - let selector = format!(r#"ul > li a[href="/crate/hexponent/{version}"]"#); - let anchor = page - .select(&selector) - .unwrap() - .find(|a| a.text_contents().trim() == version) - .unwrap(); - let attributes = anchor.as_node().as_element().unwrap().attributes.borrow(); - let classes = attributes.get("class").unwrap(); - Ok(classes.split(' ').all(|c| c != "warn")) + .create_async() + .await?; + let web = env.web_app().await; + + let status = |version| { + let web = web.clone(); + async move { + let page = kuchikiki::parse_html() + .one(web.get("/crate/hexponent/0.3.0").await?.text().await?); + let selector = format!(r#"ul > li a[href="/crate/hexponent/{version}"]"#); + let anchor = page + .select(&selector) + .unwrap() + .find(|a| a.text_contents().trim() == version) + .unwrap(); + let attributes = anchor.as_node().as_element().unwrap().attributes.borrow(); + let classes = attributes.get("class").unwrap(); + Ok::<_, anyhow::Error>(classes.split(' ').all(|c| c != "warn")) + } }; - assert!(status("0.3.0")?); - assert!(!status("0.2.0")?); + assert!(status("0.3.0").await?); + assert!(!status("0.2.0").await?); Ok(()) }) } @@ -2125,19 +2233,23 @@ mod test { #[test_case(true)] #[test_case(false)] fn test_no_trailing_rustdoc_slash(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("tokio") .version("0.2.21") .archive_storage(archive_storage) .rustdoc_file("tokio/time/index.html") - .create()?; - - assert_redirect( - "/tokio/0.2.21/tokio/time", - "/tokio/0.2.21/tokio/time/index.html", - env.frontend(), - )?; + .create_async() + .await?; + + env.web_app() + .await + .assert_redirect( + "/tokio/0.2.21/tokio/time", + "/tokio/0.2.21/tokio/time/index.html", + ) + .await?; Ok(()) }) @@ -2146,42 +2258,51 @@ mod test { #[test_case(true)] #[test_case(false)] fn test_non_ascii(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("const_unit_poc") .version("1.0.0") .archive_storage(archive_storage) .rustdoc_file("const_unit_poc/units/constant.Ω.html") - .create()?; - assert_success( - "/const_unit_poc/1.0.0/const_unit_poc/units/constant.Ω.html", - env.frontend(), - ) + .create_async() + .await?; + env.web_app() + .await + .assert_success(&encode_url_path( + "/const_unit_poc/1.0.0/const_unit_poc/units/constant.Ω.html", + )) + .await?; + Ok(()) }) } #[test_case(true)] #[test_case(false)] fn test_latest_version_keeps_query(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("tungstenite") .version("0.10.0") .archive_storage(archive_storage) .rustdoc_file("tungstenite/index.html") - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("tungstenite") .version("0.11.0") .archive_storage(archive_storage) .rustdoc_file("tungstenite/index.html") - .create()?; + .create_async() + .await?; assert_eq!( latest_version_redirect( "/tungstenite/0.10.0/tungstenite/?search=String%20-%3E%20Message", - env.frontend(), + &env.web_app().await, &env.config() - )?, + ).await?, "/crate/tungstenite/latest/target-redirect/x86_64-unknown-linux-gnu/tungstenite/index.html?search=String%20-%3E%20Message", ); Ok(()) @@ -2191,28 +2312,35 @@ mod test { #[test_case(true)] #[test_case(false)] fn latest_version_works_when_source_deleted(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("pyo3") .version("0.2.7") .archive_storage(archive_storage) .source_file("src/objects/exc.rs", b"//! some docs") - .create()?; - env.fake_release().name("pyo3").version("0.13.2").create()?; + .create_async() + .await?; + env.async_fake_release() + .await + .name("pyo3") + .version("0.13.2") + .create_async() + .await?; let target_redirect = "/crate/pyo3/latest/target-redirect/x86_64-unknown-linux-gnu/src/pyo3/objects/exc.rs.html"; + let web = env.web_app().await; assert_eq!( latest_version_redirect( "/pyo3/0.2.7/src/pyo3/objects/exc.rs.html", - env.frontend(), + &web, &env.config(), - )?, + ) + .await?, target_redirect ); - assert_redirect( - target_redirect, - "/pyo3/latest/pyo3/?search=exc", - env.frontend(), - )?; + + web.assert_redirect(target_redirect, "/pyo3/latest/pyo3/?search=exc") + .await?; Ok(()) }) } @@ -2229,30 +2357,35 @@ mod test { #[test_case(true)] #[test_case(false)] fn test_version_link_goes_to_docs(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("hexponent") .version("0.3.0") .archive_storage(archive_storage) .rustdoc_file("hexponent/index.html") - .create()?; - env.fake_release() + .create_async() + .await?; + env.async_fake_release() + .await .name("hexponent") .version("0.3.1") .archive_storage(archive_storage) .rustdoc_file("hexponent/index.html") .rustdoc_file("hexponent/something.html") - .create()?; + .create_async() + .await?; // test rustdoc pages stay on the documentation let releases_response = env - .frontend() + .web_app() + .await .get("/crate/hexponent/0.3.1/menus/releases") - .send()?; + .await?; assert!(releases_response.status().is_success()); - assert_cache_control(&releases_response, CachePolicy::ForeverInCdn, &env.config()); + releases_response.assert_cache_control(CachePolicy::ForeverInCdn, &env.config()); assert_eq!( - parse_release_links_from_menu(&releases_response.text()?), + parse_release_links_from_menu(&releases_response.text().await?), vec![ "/crate/hexponent/0.3.1/target-redirect/hexponent/index.html".to_owned(), "/crate/hexponent/0.3.0/target-redirect/hexponent/index.html".to_owned(), @@ -2261,13 +2394,14 @@ mod test { // test if target-redirect inludes path let releases_response = env - .frontend() + .web_app() + .await .get("/crate/hexponent/0.3.1/menus/releases/hexponent/something.html") - .send()?; + .await?; assert!(releases_response.status().is_success()); - assert_cache_control(&releases_response, CachePolicy::ForeverInCdn, &env.config()); + releases_response.assert_cache_control(CachePolicy::ForeverInCdn, &env.config()); assert_eq!( - parse_release_links_from_menu(&releases_response.text()?), + parse_release_links_from_menu(&releases_response.text().await?), vec![ "/crate/hexponent/0.3.1/target-redirect/hexponent/something.html".to_owned(), "/crate/hexponent/0.3.0/target-redirect/hexponent/something.html".to_owned(), @@ -2276,10 +2410,12 @@ mod test { // test /crate pages stay on /crate let page = kuchikiki::parse_html().one( - env.frontend() - .get("/crate/hexponent/0.3.0/") - .send()? - .text()?, + env.web_app() + .await + .get("/crate/hexponent/0.3.0") + .await? + .text() + .await?, ); let selector = r#"ul > li a[href="/crate/hexponent/0.3.1"]"#.to_string(); assert_eq!( @@ -2294,19 +2430,23 @@ mod test { #[test] fn test_repository_link_in_topbar_dropdown() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("testing") .repo("https://git.example.com") .version("0.1.0") .rustdoc_file("testing/index.html") - .create()?; + .create_async() + .await?; let dom = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/testing/0.1.0/testing/") - .send()? - .text()?, + .await? + .text() + .await?, ); assert_eq!( @@ -2322,19 +2462,23 @@ mod test { #[test] fn test_repository_link_in_topbar_dropdown_github() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("testing") .version("0.1.0") .rustdoc_file("testing/index.html") .github_stats("https://git.example.com", 123, 321, 333) - .create()?; + .create_async() + .await?; let dom = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/testing/0.1.0/testing/") - .send()? - .text()?, + .await? + .text() + .await?, ); assert_eq!( @@ -2350,8 +2494,9 @@ mod test { #[test] fn test_owner_links_with_team() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("testing") .version("0.1.0") .add_owner(CrateOwner { @@ -2364,13 +2509,16 @@ mod test { kind: OwnerKind::Team, avatar: "".into(), }) - .create()?; + .create_async() + .await?; let dom = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/testing/0.1.0/testing/") - .send()? - .text()?, + .await? + .text() + .await?, ); let owner_links: Vec<_> = dom @@ -2404,8 +2552,9 @@ mod test { #[test] fn test_dependency_optional_suffix() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("testing") .version("0.1.0") .rustdoc_file("testing/index.html") @@ -2413,20 +2562,29 @@ mod test { Dependency::new("optional-dep".to_string(), "1.2.3".to_string()) .set_optional(true), ) - .create()?; + .create_async() + .await?; let dom = kuchikiki::parse_html().one( - env.frontend() + env.web_app() + .await .get("/testing/0.1.0/testing/") - .send()? - .text()?, + .await? + .text() + .await?, ); assert!(dom .select(r#"a[href="/optional-dep/1.2.3"] > i[class="dependencies normal"] + i"#) .expect("shoud have optional dependency") .any(|el| { el.text_contents().contains("optional") })); - let dom = kuchikiki::parse_html() - .one(env.frontend().get("/crate/testing/0.1.0").send()?.text()?); + let dom = kuchikiki::parse_html().one( + env.web_app() + .await + .get("/crate/testing/0.1.0") + .await? + .text() + .await?, + ); assert!(dom .select( r#"a[href="/crate/optional-dep/1.2.3"] > i[class="dependencies normal"] + i"# @@ -2440,20 +2598,25 @@ mod test { #[test_case(true)] #[test_case(false)] fn test_missing_target_redirects_to_search(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("winapi") .version("0.3.9") .archive_storage(archive_storage) .rustdoc_file("winapi/macro.ENUM.html") - .create()?; + .create_async() + .await?; - assert_redirect( + let web = env.web_app().await; + web.assert_redirect( "/winapi/0.3.9/x86_64-unknown-linux-gnu/winapi/macro.ENUM.html", "/winapi/0.3.9/winapi/macro.ENUM.html", - env.frontend(), - )?; - assert_not_found("/winapi/0.3.9/winapi/struct.not_here.html", env.frontend())?; + ) + .await?; + + web.assert_not_found("/winapi/0.3.9/winapi/struct.not_here.html") + .await?; Ok(()) }) @@ -2462,45 +2625,53 @@ mod test { #[test_case(true)] #[test_case(false)] fn test_redirect_source_not_rust(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("winapi") .version("0.3.8") .archive_storage(archive_storage) .source_file("src/docs.md", b"created by Peter Rabbit") - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("winapi") .version("0.3.9") .archive_storage(archive_storage) - .create()?; + .create_async() + .await?; - assert_success("/winapi/0.3.8/src/winapi/docs.md.html", env.frontend())?; + let web = env.web_app().await; + web.assert_success("/winapi/0.3.8/src/winapi/docs.md.html") + .await?; // people can end up here from clicking "go to latest" while in source view - assert_redirect( + web.assert_redirect( "/crate/winapi/0.3.9/target-redirect/src/winapi/docs.md.html", "/winapi/0.3.9/winapi/", - env.frontend(), - )?; + ) + .await?; Ok(()) }) } #[test] fn noindex_nonlatest() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .rustdoc_file("dummy/index.html") - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; assert!(web .get("/dummy/0.1.0/dummy/") - .send()? + .await? .headers() .get("x-robots-tag") .unwrap() @@ -2510,7 +2681,7 @@ mod test { assert!(web .get("/dummy/latest/dummy/") - .send()? + .await? .headers() .get("x-robots-tag") .is_none()); @@ -2520,11 +2691,11 @@ mod test { #[test] fn download_unknown_version_404() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; - let response = web.get("/crate/dummy/0.1.0/download").send()?; - assert_cache_control(&response, CachePolicy::NoCaching, &env.config()); + let response = web.get("/crate/dummy/0.1.0/download").await?; + response.assert_cache_control(CachePolicy::NoCaching, &env.config()); assert_eq!(response.status(), StatusCode::NOT_FOUND); Ok(()) }); @@ -2532,17 +2703,19 @@ mod test { #[test] fn download_old_storage_version_404() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(false) - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; - let response = web.get("/crate/dummy/0.1.0/download").send()?; - assert_cache_control(&response, CachePolicy::NoCaching, &env.config()); + let response = web.get("/crate/dummy/0.1.0/download").await?; + response.assert_cache_control(CachePolicy::NoCaching, &env.config()); assert_eq!(response.status(), StatusCode::NOT_FOUND); Ok(()) }); @@ -2550,88 +2723,108 @@ mod test { #[test] fn download_semver() { - wrapper(|env| { + async_wrapper(|env| async move { env.override_config(|config| { config.s3_static_root_path = "https://static.docs.rs".into() }); - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(true) - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; - assert_redirect_cached_unchecked( + web.assert_redirect_cached_unchecked( "/crate/dummy/0.1/download", "https://static.docs.rs/rustdoc/dummy/0.1.0.zip", CachePolicy::ForeverInCdn, - web, &env.config(), - )?; - assert!(env.storage().get_public_access("rustdoc/dummy/0.1.0.zip")?); + ) + .await?; + assert!( + env.async_storage() + .await + .get_public_access("rustdoc/dummy/0.1.0.zip") + .await? + ); Ok(()) }); } #[test] fn download_specific_version() { - wrapper(|env| { + async_wrapper(|env| async move { env.override_config(|config| { config.s3_static_root_path = "https://static.docs.rs".into() }); - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(true) - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; + let storage = env.async_storage().await; // disable public access to be sure that the handler will enable it - env.storage() - .set_public_access("rustdoc/dummy/0.1.0.zip", false)?; + storage + .set_public_access("rustdoc/dummy/0.1.0.zip", false) + .await?; - assert_redirect_cached_unchecked( + web.assert_redirect_cached_unchecked( "/crate/dummy/0.1.0/download", "https://static.docs.rs/rustdoc/dummy/0.1.0.zip", CachePolicy::ForeverInCdn, - web, &env.config(), - )?; - assert!(env.storage().get_public_access("rustdoc/dummy/0.1.0.zip")?); + ) + .await?; + assert!(storage.get_public_access("rustdoc/dummy/0.1.0.zip").await?); Ok(()) }); } #[test] fn download_latest_version() { - wrapper(|env| { + async_wrapper(|env| async move { env.override_config(|config| { config.s3_static_root_path = "https://static.docs.rs".into() }); - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(true) - .create()?; + .create_async() + .await?; - env.fake_release() + env.async_fake_release() + .await .name("dummy") .version("0.2.0") .archive_storage(true) - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; - assert_redirect_cached_unchecked( + web.assert_redirect_cached_unchecked( "/crate/dummy/latest/download", "https://static.docs.rs/rustdoc/dummy/0.2.0.zip", CachePolicy::ForeverInCdn, - web, &env.config(), - )?; - assert!(env.storage().get_public_access("rustdoc/dummy/0.2.0.zip")?); + ) + .await?; + assert!( + env.async_storage() + .await + .get_public_access("rustdoc/dummy/0.2.0.zip") + .await? + ); Ok(()) }); } @@ -2639,18 +2832,20 @@ mod test { #[test_case("something.js")] #[test_case("someting.css")] fn serve_release_specific_static_assets(name: &str) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(true) .rustdoc_file_with(name, b"content") - .create()?; + .create_async() + .await?; - let web = env.frontend(); - let response = web.get(&format!("/dummy/0.1.0/{name}")).send()?; + let web = env.web_app().await; + let response = web.get(&format!("/dummy/0.1.0/{name}")).await?; assert!(response.status().is_success()); - assert_eq!(response.text()?, "content"); + assert_eq!(response.text().await?, "content"); Ok(()) }) @@ -2660,28 +2855,31 @@ mod test { #[test_case("settings-1234.js")] fn fallback_to_root_storage_for_some_js_assets(path: &str) { // test workaround for https://github.com/rust-lang/docs.rs/issues/1979 - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("dummy") .version("0.1.0") .archive_storage(true) - .create()?; + .create_async() + .await?; - env.storage().store_one("asset.js", *b"content")?; - env.storage().store_one(path, *b"more_content")?; + let storage = env.async_storage().await; + storage.store_one("asset.js", *b"content").await?; + storage.store_one(path, *b"more_content").await?; - let web = env.frontend(); + let web = env.web_app().await; assert_eq!( - web.get("/dummy/0.1.0/asset.js").send()?.status(), + web.get("/dummy/0.1.0/asset.js").await?.status(), StatusCode::NOT_FOUND ); - assert!(web.get("/asset.js").send()?.status().is_success()); + assert!(web.get("/asset.js").await?.status().is_success()); - assert!(web.get(&format!("/{path}")).send()?.status().is_success()); - let response = web.get(&format!("/dummy/0.1.0/{path}")).send()?; + assert!(web.get(&format!("/{path}")).await?.status().is_success()); + let response = web.get(&format!("/dummy/0.1.0/{path}")).await?; assert!(response.status().is_success()); - assert_eq!(response.text()?, "more_content"); + assert_eq!(response.text().await?, "more_content"); Ok(()) }) @@ -2689,21 +2887,22 @@ mod test { #[test] fn redirect_with_encoded_chars_in_path() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("clap") .version("2.24.0") .archive_storage(true) - .create()?; - let web = env.frontend(); + .create_async() + .await?; + let web = env.web_app().await; - assert_redirect_cached_unchecked( + web.assert_redirect_cached_unchecked( "/clap/2.24.0/i686-pc-windows-gnu/clap/which%20is%20a%20part%20of%20%5B%60Display%60%5D", "/crate/clap/2.24.0/target-redirect/i686-pc-windows-gnu/clap/which%20is%20a%20part%20of%20[%60Display%60]/index.html", CachePolicy::ForeverInCdn, - web, &env.config(), - )?; + ).await?; Ok(()) }) @@ -2711,21 +2910,22 @@ mod test { #[test] fn search_with_encoded_chars_in_path() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("clap") .version("2.24.0") .archive_storage(true) - .create()?; - let web = env.frontend(); + .create_async() + .await?; + let web = env.web_app().await; - assert_redirect_cached_unchecked( + web.assert_redirect_cached_unchecked( "/clap/latest/clapproc%20macro%20%60Parser%60%20not%20expanded:%20Cannot%20create%20expander%20for", "/clap/latest/clapproc%20macro%20%60Parser%60%20not%20expanded:%20Cannot%20create%20expander%20for/clap/", CachePolicy::ForeverInCdn, - web, &env.config(), - )?; + ).await?; Ok(()) }) @@ -2734,22 +2934,19 @@ mod test { #[test_case("/something/1.2.3/some_path/", "/crate/something/1.2.3")] #[test_case("/something/latest/some_path/", "/crate/something/latest")] fn rustdoc_page_from_failed_build_redirects_to_crate(path: &str, expected: &str) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("something") .version("1.2.3") .archive_storage(true) .build_result_failed() - .create()?; - let web = env.frontend(); + .create_async() + .await?; + let web = env.web_app().await; - assert_redirect_cached( - path, - expected, - CachePolicy::ForeverInCdn, - web, - &env.config(), - )?; + web.assert_redirect_cached(path, expected, CachePolicy::ForeverInCdn, &env.config()) + .await?; Ok(()) }) diff --git a/src/web/sitemap.rs b/src/web/sitemap.rs index ec82e2cd1..6cc9caadf 100644 --- a/src/web/sitemap.rs +++ b/src/web/sitemap.rs @@ -210,7 +210,8 @@ mod tests { fn sitemap_index() { async_wrapper(|env| async move { let app = env.web_app().await; - app.assert_success("/sitemap.xml").await + app.assert_success("/sitemap.xml").await?; + Ok(()) }) } @@ -260,7 +261,7 @@ mod tests { let response = web.get("/-/sitemap/s/sitemap.xml").await?; assert!(response.status().is_success()); - let content = response.text().await; + let content = response.text().await?; assert!(content.contains("some_random_crate")); assert!(!(content.contains("some_random_crate_that_failed"))); @@ -269,7 +270,7 @@ mod tests { let response = web.get(&format!("/-/sitemap/{letter}/sitemap.xml")).await?; assert!(response.status().is_success()); - assert!(!(response.text().await.contains("some_random_crate"))); + assert!(!(response.text().await?.contains("some_random_crate"))); } Ok(()) @@ -292,7 +293,7 @@ mod tests { let response = web.get("/-/sitemap/s/sitemap.xml").await?; assert!(response.status().is_success()); - let content = response.text().await; + let content = response.text().await?; assert!(content.contains("2022-08-28T00:00:00+00:00")); Ok(()) }) @@ -315,7 +316,8 @@ mod tests { let path = format!("/about/{filename}"); web.assert_success(&path).await?; } - web.assert_success("/about").await + web.assert_success("/about").await?; + Ok(()) }) } diff --git a/src/web/source.rs b/src/web/source.rs index dc8887170..6ee1c7507 100644 --- a/src/web/source.rs +++ b/src/web/source.rs @@ -345,8 +345,10 @@ pub(crate) async fn source_browser_handler( #[cfg(test)] mod tests { - use crate::test::*; - use crate::web::cache::CachePolicy; + use crate::{ + test::{async_wrapper, AxumResponseTestExt, AxumRouterTestExt}, + web::{cache::CachePolicy, encode_url_path}, + }; use kuchikiki::traits::TendrilSink; use reqwest::StatusCode; use test_case::test_case; @@ -366,26 +368,31 @@ mod tests { #[test_case(true)] #[test_case(false)] fn fetch_source_file_utf8_path(archive_storage: bool) { - wrapper(|env| { + async_wrapper(|env| async move { let filename = "序.pdf"; - env.fake_release() + env.async_fake_release() + .await .archive_storage(archive_storage) .name("fake") .version("0.1.0") .source_file(filename, b"some_random_content") - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; let response = web - .get(&format!("/crate/fake/0.1.0/source/{filename}")) - .send()?; + .get(&format!( + "/crate/fake/0.1.0/source/{}", + encode_url_path(filename) + )) + .await?; assert!(response.status().is_success()); assert_eq!( response.headers().get("link").unwrap(), "; rel=\"canonical\"", ); - assert!(response.text()?.contains("some_random_content")); + assert!(response.text().await?.contains("some_random_content")); Ok(()) }); } @@ -393,34 +400,31 @@ mod tests { #[test_case(true)] #[test_case(false)] fn fetch_source_file_content(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .archive_storage(archive_storage) .name("fake") .version("0.1.0") .source_file("some_filename.rs", b"some_random_content") - .create()?; - let web = env.frontend(); - assert_success_cached( + .create_async() + .await?; + let web = env.web_app().await; + web.assert_success_cached( "/crate/fake/0.1.0/source/", - web, CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config(), - )?; - let response = web - .get("/crate/fake/0.1.0/source/some_filename.rs") - .send()?; + ) + .await?; + let response = web.get("/crate/fake/0.1.0/source/some_filename.rs").await?; assert!(response.status().is_success()); assert_eq!( response.headers().get("link").unwrap(), "; rel=\"canonical\"" ); - assert_cache_control( - &response, - CachePolicy::ForeverInCdnAndStaleInBrowser, - &env.config(), - ); - assert!(response.text()?.contains("some_random_content")); + response + .assert_cache_control(CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config()); + assert!(response.text().await?.contains("some_random_content")); Ok(()) }); } @@ -428,15 +432,17 @@ mod tests { #[test_case(true)] #[test_case(false)] fn fetch_binary(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .archive_storage(archive_storage) .name("fake") .version("0.1.0") .source_file("some_file.pdf", b"some_random_content") - .create()?; - let web = env.frontend(); - let response = web.get("/crate/fake/0.1.0/source/some_file.pdf").send()?; + .create_async() + .await?; + let web = env.web_app().await; + let response = web.get("/crate/fake/0.1.0/source/some_file.pdf").await?; assert!(response.status().is_success()); assert_eq!( response.headers().get("link").unwrap(), @@ -452,12 +458,9 @@ mod tests { "application/pdf" ); - assert_cache_control( - &response, - CachePolicy::ForeverInCdnAndStaleInBrowser, - &env.config(), - ); - assert!(response.text()?.contains("some_random_content")); + response + .assert_cache_control(CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config()); + assert!(response.text().await?.contains("some_random_content")); Ok(()) }); } @@ -465,16 +468,18 @@ mod tests { #[test_case(true)] #[test_case(false)] fn cargo_ok_not_skipped(archive_storage: bool) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .archive_storage(archive_storage) .name("fake") .version("0.1.0") .source_file(".cargo-ok", b"ok") .source_file("README.md", b"hello") - .create()?; - let web = env.frontend(); - assert_success("/crate/fake/0.1.0/source/", web)?; + .create_async() + .await?; + let web = env.web_app().await; + web.assert_success("/crate/fake/0.1.0/source/").await?; Ok(()) }); } @@ -482,32 +487,32 @@ mod tests { #[test_case(true)] #[test_case(false)] fn empty_file_list_dont_break_the_view(archive_storage: bool) { - wrapper(|env| { + async_wrapper(|env| async move { let release_id = env - .fake_release() + .async_fake_release() + .await .archive_storage(archive_storage) .name("fake") .version("0.1.0") .source_file("README.md", b"hello") - .create()?; + .create_async() + .await?; let path = "/crate/fake/0.1.0/source/README.md"; - let web = env.frontend(); - assert_success(path, web)?; + let web = env.web_app().await; + web.assert_success(path).await?; - env.runtime().block_on(async { - let mut conn = env.async_db().await.async_conn().await; - sqlx::query!( - "UPDATE releases + let mut conn = env.async_db().await.async_conn().await; + sqlx::query!( + "UPDATE releases SET files = NULL WHERE id = $1", - release_id.0, - ) - .execute(&mut *conn) - .await - })?; + release_id.0, + ) + .execute(&mut *conn) + .await?; - assert!(web.get(path).send()?.status().is_success()); + assert!(web.get(path).await?.status().is_success()); Ok(()) }); @@ -515,18 +520,23 @@ mod tests { #[test] fn latest_contains_links_to_latest() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .archive_storage(true) .name("fake") .version("0.1.0") .source_file(".cargo-ok", b"ok") .source_file("README.md", b"hello") - .create()?; - let resp = env.frontend().get("/crate/fake/latest/source/").send()?; - assert_cache_control(&resp, CachePolicy::ForeverInCdn, &env.config()); - assert!(resp.url().as_str().ends_with("/crate/fake/latest/source/")); - let body = String::from_utf8(resp.bytes().unwrap().to_vec()).unwrap(); + .create_async() + .await?; + let resp = env + .web_app() + .await + .get("/crate/fake/latest/source/") + .await?; + resp.assert_cache_control(CachePolicy::ForeverInCdn, &env.config()); + let body = resp.text().await?; assert!(body.contains(""#)); let response = web .get("/crate/fake/0.1.0/source/Cargo.lock") - .send()? - .text()?; + .await? + .text() + .await?; assert!(response.contains(r#""#)); Ok(()) @@ -649,19 +672,22 @@ mod tests { #[test] fn dotfiles_with_extension_are_highlighted() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("fake") .version("0.1.0") .source_file(".rustfmt.toml", b"[rustfmt]") - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; let response = web .get("/crate/fake/0.1.0/source/.rustfmt.toml") - .send()? - .text()?; + .await? + .text() + .await?; assert!(response.contains(r#""#)); Ok(()) @@ -670,17 +696,19 @@ mod tests { #[test] fn json_is_served_as_rendered_html() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("fake") .version("0.1.0") .source_file("Cargo.toml", b"") .source_file("config.json", b"{}") - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; - let response = web.get("/crate/fake/0.1.0/source/config.json").send()?; + let response = web.get("/crate/fake/0.1.0/source/config.json").await?; assert!(response .headers() .get("content-type") @@ -689,7 +717,7 @@ mod tests { .unwrap() .starts_with("text/html")); - let text = response.text()?; + let text = response.text().await?; assert!(text.starts_with(r#""#)); // file list doesn't show "../" @@ -704,27 +732,26 @@ mod tests { #[test] fn root_file_list() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("fake") .version("0.1.0") .source_file("Cargo.toml", b"some_random_content") .source_file("folder1/some_filename.rs", b"some_random_content") .source_file("folder2/another_filename.rs", b"some_random_content") .source_file("root_filename.rs", b"some_random_content") - .create()?; + .create_async() + .await?; - let web = env.frontend(); - let response = web.get("/crate/fake/0.1.0/source/").send()?; + let web = env.web_app().await; + let response = web.get("/crate/fake/0.1.0/source/").await?; assert!(response.status().is_success()); - assert_cache_control( - &response, - CachePolicy::ForeverInCdnAndStaleInBrowser, - &env.config(), - ); + response + .assert_cache_control(CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config()); assert_eq!( - get_file_list_links(&response.text()?), + get_file_list_links(&response.text().await?), vec![ "./folder1/", "./folder2/", @@ -738,29 +765,28 @@ mod tests { #[test] fn child_file_list() { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("fake") .version("0.1.0") .source_file("folder1/some_filename.rs", b"some_random_content") .source_file("folder1/more_filenames.rs", b"some_random_content") .source_file("folder2/another_filename.rs", b"some_random_content") .source_file("root_filename.rs", b"some_random_content") - .create()?; + .create_async() + .await?; - let web = env.frontend(); + let web = env.web_app().await; let response = web .get("/crate/fake/0.1.0/source/folder1/some_filename.rs") - .send()?; + .await?; assert!(response.status().is_success()); - assert_cache_control( - &response, - CachePolicy::ForeverInCdnAndStaleInBrowser, - &env.config(), - ); + response + .assert_cache_control(CachePolicy::ForeverInCdnAndStaleInBrowser, &env.config()); assert_eq!( - get_file_list_links(&response.text()?), + get_file_list_links(&response.text().await?), vec!["../", "./more_filenames.rs", "./some_filename.rs"], ); Ok(()) @@ -769,22 +795,25 @@ mod tests { #[test] fn large_file_test() { - wrapper(|env| { + async_wrapper(|env| async move { env.override_config(|config| { config.max_file_size = 1; config.max_file_size_html = 1; }); - env.fake_release() + env.async_fake_release() + .await .name("fake") .version("0.1.0") .source_file("large_file.rs", b"some_random_content") - .create()?; + .create_async() + .await?; - let web = env.frontend(); - let response = web.get("/crate/fake/0.1.0/source/large_file.rs").send()?; + let web = env.web_app().await; + let response = web.get("/crate/fake/0.1.0/source/large_file.rs").await?; assert_eq!(response.status(), StatusCode::OK); assert!(response - .text()? + .text() + .await? .contains("This file is too large to display")); Ok(()) }); diff --git a/src/web/statics.rs b/src/web/statics.rs index 339f5d6f7..57fbd4dd5 100644 --- a/src/web/statics.rs +++ b/src/web/statics.rs @@ -81,29 +81,40 @@ pub(crate) fn build_static_router() -> AxumRouter { mod tests { use super::{STYLE_CSS, VENDORED_CSS}; use crate::{ - test::{assert_cache_control, wrapper}, + test::{async_wrapper, AxumResponseTestExt, AxumRouterTestExt}, web::cache::CachePolicy, }; + use axum::response::Response as AxumResponse; use reqwest::StatusCode; use std::fs; use test_case::test_case; const STATIC_SEARCH_PATHS: &[&str] = &["static", "vendor"]; + fn content_length(resp: &AxumResponse) -> u64 { + resp.headers() + .get("Content-Length") + .expect("content-length header") + .to_str() + .unwrap() + .parse() + .unwrap() + } + #[test] fn style_css() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; - let resp = web.get("/-/static/style.css").send()?; + let resp = web.get("/-/static/style.css").await?; assert!(resp.status().is_success()); - assert_cache_control(&resp, CachePolicy::ForeverInCdnAndBrowser, &env.config()); + resp.assert_cache_control(CachePolicy::ForeverInCdnAndBrowser, &env.config()); assert_eq!( resp.headers().get("Content-Type"), Some(&"text/css".parse().unwrap()), ); - assert_eq!(resp.content_length().unwrap(), STYLE_CSS.len() as u64); - assert_eq!(resp.bytes()?, STYLE_CSS.as_bytes()); + assert_eq!(content_length(&resp), STYLE_CSS.len() as u64); + assert_eq!(resp.bytes().await?, STYLE_CSS.as_bytes()); Ok(()) }); @@ -111,18 +122,18 @@ mod tests { #[test] fn vendored_css() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; - let resp = web.get("/-/static/vendored.css").send()?; + let resp = web.get("/-/static/vendored.css").await?; assert!(resp.status().is_success()); - assert_cache_control(&resp, CachePolicy::ForeverInCdnAndBrowser, &env.config()); + resp.assert_cache_control(CachePolicy::ForeverInCdnAndBrowser, &env.config()); assert_eq!( resp.headers().get("Content-Type"), Some(&"text/css".parse().unwrap()), ); - assert_eq!(resp.content_length().unwrap(), VENDORED_CSS.len() as u64); - assert_eq!(resp.text()?, VENDORED_CSS); + assert_eq!(content_length(&resp), VENDORED_CSS.len() as u64); + assert_eq!(resp.text().await?, VENDORED_CSS); Ok(()) }); @@ -130,16 +141,16 @@ mod tests { #[test] fn io_error_not_a_directory_leads_to_404() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; // just to be sure that `index.js` exists - assert!(web.get("/-/static/index.js").send()?.status().is_success()); + assert!(web.get("/-/static/index.js").await?.status().is_success()); // `index.js` exists, but is not a directory, // so trying to fetch it via `ServeDir` will lead // to an IO-error. - let resp = web.get("/-/static/index.js/something").send()?; + let resp = web.get("/-/static/index.js/something").await?; assert_eq!(resp.status().as_u16(), StatusCode::NOT_FOUND); Ok(()) @@ -151,18 +162,18 @@ mod tests { #[test_case("/-/static/keyboard.js", "handleKey")] #[test_case("/-/static/source.js", "toggleSource")] fn js_content(path: &str, expected_content: &str) { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; - let resp = web.get(path).send()?; + let resp = web.get(path).await?; assert!(resp.status().is_success()); - assert_cache_control(&resp, CachePolicy::ForeverInCdnAndBrowser, &env.config()); + resp.assert_cache_control(CachePolicy::ForeverInCdnAndBrowser, &env.config()); assert_eq!( resp.headers().get("Content-Type"), Some(&"text/javascript".parse().unwrap()), ); - assert!(resp.content_length().unwrap() > 10); - assert!(resp.text()?.contains(expected_content)); + assert!(content_length(&resp) > 10); + assert!(resp.text().await?.contains(expected_content)); Ok(()) }); @@ -170,8 +181,8 @@ mod tests { #[test] fn static_files() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; for root in STATIC_SEARCH_PATHS { for entry in walkdir::WalkDir::new(root) { @@ -183,12 +194,12 @@ mod tests { let path = entry.path(); let url = format!("/-/static/{}", file.to_str().unwrap()); - let resp = web.get(&url).send()?; + let resp = web.get(&url).await?; assert!(resp.status().is_success(), "failed to fetch {url:?}"); - assert_cache_control(&resp, CachePolicy::ForeverInCdnAndBrowser, &env.config()); + resp.assert_cache_control(CachePolicy::ForeverInCdnAndBrowser, &env.config()); assert_eq!( - resp.bytes()?, + resp.bytes().await?, fs::read(path).unwrap(), "failed to fetch {url:?}", ); @@ -201,9 +212,9 @@ mod tests { #[test] fn static_file_that_doesnt_exist() { - wrapper(|env| { - let response = env.frontend().get("/-/static/whoop-de-do.png").send()?; - assert_cache_control(&response, CachePolicy::NoCaching, &env.config()); + async_wrapper(|env| async move { + let response = env.web_app().await.get("/-/static/whoop-de-do.png").await?; + response.assert_cache_control(CachePolicy::NoCaching, &env.config()); assert_eq!(response.status(), StatusCode::NOT_FOUND); Ok(()) @@ -212,14 +223,14 @@ mod tests { #[test] fn static_mime_types() { - wrapper(|env| { - let web = env.frontend(); + async_wrapper(|env| async move { + let web = env.web_app().await; let files = &[("vendored.css", "text/css")]; for (file, mime) in files { let url = format!("/-/static/{file}"); - let resp = web.get(&url).send()?; + let resp = web.get(&url).await?; assert_eq!( resp.headers().get("Content-Type"), diff --git a/src/web/status.rs b/src/web/status.rs index 69d188a82..c65f36e55 100644 --- a/src/web/status.rs +++ b/src/web/status.rs @@ -46,10 +46,8 @@ pub(crate) async fn status_handler( #[cfg(test)] mod tests { - use crate::{ - test::{assert_cache_control, assert_redirect, wrapper}, - web::cache::CachePolicy, - }; + use crate::test::{async_wrapper, AxumResponseTestExt, AxumRouterTestExt}; + use crate::web::cache::CachePolicy; use reqwest::StatusCode; use test_case::test_case; @@ -58,17 +56,23 @@ mod tests { #[test_case("0.1.0")] #[test_case("=0.1.0"; "exact_version")] fn status(version: &str) { - wrapper(|env| { - env.fake_release().name("foo").version("0.1.0").create()?; + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("foo") + .version("0.1.0") + .create_async() + .await?; let response = env - .frontend() - .get(&format!("/crate/foo/{version}/status.json")) - .send()?; - assert_cache_control(&response, CachePolicy::NoStoreMustRevalidate, &env.config()); + .web_app() + .await + .get_and_follow_redirects(&format!("/crate/foo/{version}/status.json")) + .await?; + response.assert_cache_control(CachePolicy::NoStoreMustRevalidate, &env.config()); assert_eq!(response.headers()["access-control-allow-origin"], "*"); assert_eq!(response.status(), StatusCode::OK); - let value: serde_json::Value = serde_json::from_str(&response.text()?)?; + let value: serde_json::Value = serde_json::from_str(&response.text().await?)?; assert_eq!( value, @@ -84,15 +88,19 @@ mod tests { #[test] fn redirect_latest() { - wrapper(|env| { - env.fake_release().name("foo").version("0.1.0").create()?; - - let redirect = assert_redirect( - "/crate/foo/*/status.json", - "/crate/foo/latest/status.json", - env.frontend(), - )?; - assert_cache_control(&redirect, CachePolicy::NoStoreMustRevalidate, &env.config()); + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("foo") + .version("0.1.0") + .create_async() + .await?; + + let web = env.web_app().await; + let redirect = web + .assert_redirect("/crate/foo/*/status.json", "/crate/foo/latest/status.json") + .await?; + redirect.assert_cache_control(CachePolicy::NoStoreMustRevalidate, &env.config()); assert_eq!(redirect.headers()["access-control-allow-origin"], "*"); Ok(()) @@ -102,15 +110,22 @@ mod tests { #[test_case("0.1")] #[test_case("~0.1"; "semver")] fn redirect(version: &str) { - wrapper(|env| { - env.fake_release().name("foo").version("0.1.0").create()?; - - let redirect = assert_redirect( - &format!("/crate/foo/{version}/status.json"), - "/crate/foo/0.1.0/status.json", - env.frontend(), - )?; - assert_cache_control(&redirect, CachePolicy::NoStoreMustRevalidate, &env.config()); + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("foo") + .version("0.1.0") + .create_async() + .await?; + + let web = env.web_app().await; + let redirect = web + .assert_redirect( + &format!("/crate/foo/{version}/status.json"), + "/crate/foo/0.1.0/status.json", + ) + .await?; + redirect.assert_cache_control(CachePolicy::NoStoreMustRevalidate, &env.config()); assert_eq!(redirect.headers()["access-control-allow-origin"], "*"); Ok(()) @@ -122,21 +137,25 @@ mod tests { #[test_case("0.1.0")] #[test_case("=0.1.0"; "exact_version")] fn failure(version: &str) { - wrapper(|env| { - env.fake_release() + async_wrapper(|env| async move { + env.async_fake_release() + .await .name("foo") .version("0.1.0") .build_result_failed() - .create()?; + .create_async() + .await?; let response = env - .frontend() - .get(&format!("/crate/foo/{version}/status.json")) - .send()?; - assert_cache_control(&response, CachePolicy::NoStoreMustRevalidate, &env.config()); + .web_app() + .await + .get_and_follow_redirects(&format!("/crate/foo/{version}/status.json")) + .await?; + response.assert_cache_control(CachePolicy::NoStoreMustRevalidate, &env.config()); assert_eq!(response.headers()["access-control-allow-origin"], "*"); + dbg!(&response); assert_eq!(response.status(), StatusCode::OK); - let value: serde_json::Value = serde_json::from_str(&response.text()?)?; + let value: serde_json::Value = serde_json::from_str(&response.text().await?)?; assert_eq!( value, @@ -161,14 +180,20 @@ mod tests { #[test_case("foo", "0,1")] #[test_case("foo", "0,1,0")] fn not_found(krate: &str, version: &str) { - wrapper(|env| { - env.fake_release().name("foo").version("0.1.1").create()?; + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("foo") + .version("0.1.1") + .create_async() + .await?; let response = env - .frontend() - .get(&format!("/crate/{krate}/{version}/status.json")) - .send()?; - assert_cache_control(&response, CachePolicy::NoStoreMustRevalidate, &env.config()); + .web_app() + .await + .get_and_follow_redirects(&format!("/crate/{krate}/{version}/status.json")) + .await?; + response.assert_cache_control(CachePolicy::NoStoreMustRevalidate, &env.config()); assert_eq!(response.headers()["access-control-allow-origin"], "*"); assert_eq!(response.status(), StatusCode::NOT_FOUND); Ok(())