Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new endpoint giving simple rustdoc status for a version #2147

Merged
merged 5 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,53 +149,65 @@ fn assert_redirect_common(
}

/// 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).map(|_| ())
) -> Result<Response> {
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<()> {
) -> Result<Response> {
let redirect_response = assert_redirect_common(path, expected_target, web)?;
assert_cache_control(&redirect_response, cache_policy, config);
Ok(())
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<()> {
assert_redirect_common(path, expected_target, web)?;
pub(crate) fn assert_redirect(
path: &str,
expected_target: &str,
web: &TestFrontend,
) -> Result<Response> {
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(())
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<()> {
) -> Result<Response> {
let redirect_response = assert_redirect_common(path, expected_target, web)?;
assert_cache_control(&redirect_response, cache_policy, config);

Expand All @@ -205,7 +217,7 @@ pub(crate) fn assert_redirect_cached(
anyhow::bail!("failed to GET {expected_target}: {status}");
}

Ok(())
Ok(redirect_response)
}

pub(crate) struct TestEnvironment {
Expand Down
4 changes: 2 additions & 2 deletions src/web/builds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub(crate) async fn build_list_handler(
) -> AxumResult<impl IntoResponse> {
let (version, version_or_latest) = match match_version_axum(&pool, &name, Some(&req_version))
.await?
.assume_exact()?
.exact_name_only()?
{
MatchSemver::Exact((version, _)) => (version.clone(), version),
MatchSemver::Latest((version, _)) => (version, "latest".to_string()),
Expand Down Expand Up @@ -85,7 +85,7 @@ pub(crate) async fn build_list_json_handler(
) -> AxumResult<impl IntoResponse> {
let version = match match_version_axum(&pool, &name, Some(&req_version))
.await?
.assume_exact()?
.exact_name_only()?
{
MatchSemver::Exact((version, _)) | MatchSemver::Latest((version, _)) => version,
MatchSemver::Semver((version, _)) => {
Expand Down
2 changes: 1 addition & 1 deletion src/web/crate_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ pub(crate) async fn crate_details_handler(
let mut conn = pool.get()?;
Ok(
match_version(&mut conn, &params.name, params.version.as_deref())
.and_then(|m| m.assume_exact())?,
.and_then(|m| m.exact_name_only())?,
)
}
})
Expand Down
2 changes: 1 addition & 1 deletion src/web/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub(crate) async fn build_features_handler(
let (version, version_or_latest, is_latest_url) =
match match_version_axum(&pool, &name, Some(&req_version))
.await?
.assume_exact()?
.exact_name_only()?
{
MatchSemver::Exact((version, _)) => (version.clone(), version, false),
MatchSemver::Latest((version, _)) => (version, "latest".to_string(), true),
Expand Down
10 changes: 9 additions & 1 deletion src/web/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,21 @@ mod tests {
("/", "/"),
("/crate/hexponent/0.2.0", "/crate/:name/:version"),
("/crate/rcc/0.0.0", "/crate/:name/:version"),
(
"/crate/rcc/0.0.0/builds.json",
"/crate/:name/:version/builds.json",
),
(
"/crate/rcc/0.0.0/status.json",
"/crate/:name/:version/status.json",
),
("/-/static/index.js", "static resource"),
("/-/static/menu.js", "static resource"),
("/-/static/keyboard.js", "static resource"),
("/-/static/source.js", "static resource"),
("/-/static/opensearch.xml", "static resource"),
("/releases", "/releases"),
("/releases/feed", "static resource"),
("/releases/feed", "/releases/feed"),
("/releases/queue", "/releases/queue"),
("/releases/recent-failures", "/releases/recent-failures"),
(
Expand Down
5 changes: 3 additions & 2 deletions src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mod rustdoc;
mod sitemap;
mod source;
mod statics;
mod status;

use crate::{db::Pool, impl_axum_webpage, Context};
use anyhow::Error;
Expand Down Expand Up @@ -83,7 +84,7 @@ impl MatchVersion {
/// If the matched version was an exact match to the requested crate name, returns the
/// `MatchSemver` for the query. If the lookup required a dash/underscore conversion, returns
/// `CrateNotFound`.
fn assume_exact(self) -> Result<MatchSemver, AxumNope> {
fn exact_name_only(self) -> Result<MatchSemver, AxumNope> {
if self.corrected_name.is_none() {
Ok(self.version)
} else {
Expand Down Expand Up @@ -596,7 +597,7 @@ mod test {
fn version(v: Option<&str>, db: &TestDatabase) -> Option<String> {
let version = match_version(&mut db.conn(), "foo", v)
.ok()?
.assume_exact()
.exact_name_only()
.ok()?
.into_parts()
.0;
Expand Down
8 changes: 6 additions & 2 deletions src/web/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ pub(super) fn build_axum_routes() -> AxumRouter {
)
.route_with_tsr(
"/releases/feed",
get_static(super::releases::releases_feed_handler),
get_internal(super::releases::releases_feed_handler),
)
.route_with_tsr(
"/releases/:owner",
Expand Down Expand Up @@ -218,7 +218,11 @@ pub(super) fn build_axum_routes() -> AxumRouter {
)
.route(
"/crate/:name/:version/builds.json",
get_static(super::builds::build_list_json_handler),
get_internal(super::builds::build_list_json_handler),
)
.route(
"/crate/:name/:version/status.json",
get_internal(super::status::status_handler),
)
.route_with_tsr(
"/crate/:name/:version/builds/:id",
Expand Down
7 changes: 5 additions & 2 deletions src/web/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@ pub(crate) async fn download_handler(
) -> AxumResult<impl IntoResponse> {
let (version, _) = match_version_axum(&pool, &name, Some(&req_version))
.await?
.assume_exact()?
.exact_name_only()?
.into_parts();

let archive_path = rustdoc_archive_path(&name, &version);
Expand Down Expand Up @@ -2149,11 +2149,14 @@ mod test {
.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(),
)
)?;

Ok(())
})
}

Expand Down
177 changes: 177 additions & 0 deletions src/web/status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use super::cache::CachePolicy;
use crate::{
db::Pool,
utils::spawn_blocking,
web::{axum_redirect, error::AxumResult, match_version_axum, MatchSemver},
};
use axum::{
extract::{Extension, Path},
http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
response::IntoResponse,
Json,
};

pub(crate) async fn status_handler(
Path((name, req_version)): Path<(String, String)>,
Extension(pool): Extension<Pool>,
) -> impl IntoResponse {
(
Extension(CachePolicy::NoStoreMustRevalidate),
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
// We use an async block to emulate a try block so that we can apply the above CORS header
// and cache policy to both successful and failed responses
async move {
let (version, id) = match match_version_axum(&pool, &name, Some(&req_version))
.await?
.exact_name_only()?
{
MatchSemver::Exact((version, id)) | MatchSemver::Latest((version, id)) => {
(version, id)
}
MatchSemver::Semver((version, _)) => {
let redirect = axum_redirect(format!("/crate/{name}/{version}/status.json"))?;
return Ok(redirect.into_response());
}
};

let rustdoc_status: bool = spawn_blocking({
move || {
Ok(pool
.get()?
.query_one(
"SELECT releases.rustdoc_status
FROM releases
WHERE releases.id = $1
",
&[&id],
)?
.get("rustdoc_status"))
}
})
.await?;

let json = Json(serde_json::json!({
"version": version,
"doc_status": rustdoc_status,
}));

AxumResult::Ok(json.into_response())
}
.await,
)
}

#[cfg(test)]
mod tests {
use crate::{
test::{assert_cache_control, assert_redirect, wrapper},
web::cache::CachePolicy,
};
use reqwest::StatusCode;
use test_case::test_case;

#[test_case("latest")]
#[test_case("0.1")]
#[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()?;

let response = env
.frontend()
.get(&format!("/crate/foo/{version}/status.json"))
.send()?;
assert_cache_control(&response, 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()?)?;

assert_eq!(
value,
serde_json::json!({
"version": "0.1.0",
"doc_status": true,
})
);

Ok(())
});
}

#[test_case("0.1")]
#[test_case("*")]
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());
assert_eq!(redirect.headers()["access-control-allow-origin"], "*");

Ok(())
});
}

#[test_case("latest")]
#[test_case("0.1")]
#[test_case("0.1.0")]
#[test_case("=0.1.0"; "exact_version")]
fn failure(version: &str) {
wrapper(|env| {
env.fake_release()
.name("foo")
.version("0.1.0")
.build_result_failed()
.create()?;

let response = env
.frontend()
.get(&format!("/crate/foo/{version}/status.json"))
.send()?;
assert_cache_control(&response, 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()?)?;

assert_eq!(
value,
serde_json::json!({
"version": "0.1.0",
"doc_status": false,
})
);

Ok(())
});
}

// crate not found
#[test_case("bar", "0.1")]
#[test_case("bar", "0.1.0")]
// version not found
#[test_case("foo", "=0.1.0"; "exact_version")]
#[test_case("foo", "0.2")]
#[test_case("foo", "0.2.0")]
// invalid semver
#[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()?;

let response = env
.frontend()
.get(&format!("/crate/{krate}/{version}/status.json"))
.send()?;
assert_cache_control(&response, CachePolicy::NoStoreMustRevalidate, &env.config());
assert_eq!(response.headers()["access-control-allow-origin"], "*");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
Ok(())
});
}
}