Skip to content

Commit a11076b

Browse files
committed
controllers/krate/publish: Check deleted_crates for availability of the crate name
1 parent f0d0afd commit a11076b

File tree

3 files changed

+57
-0
lines changed

3 files changed

+57
-0
lines changed

src/controllers/krate/publish.rs

+17
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::worker::jobs::{
99
use axum::body::Bytes;
1010
use axum::Json;
1111
use cargo_manifest::{Dependency, DepsSet, TargetDepsSet};
12+
use chrono::{DateTime, SecondsFormat, Utc};
1213
use crates_io_tarball::{process_tarball, TarballError};
1314
use crates_io_worker::BackgroundJob;
1415
use diesel::connection::DefaultLoadingMode;
@@ -86,6 +87,22 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
8687
let (existing_crate, auth) = {
8788
use diesel_async::RunQueryDsl;
8889

90+
let deleted_crate: Option<(String, DateTime<Utc>)> = deleted_crates::table
91+
.filter(canon_crate_name(deleted_crates::name).eq(canon_crate_name(&metadata.name)))
92+
.filter(deleted_crates::available_at.gt(Utc::now()))
93+
.select((deleted_crates::name, deleted_crates::available_at))
94+
.first(&mut conn)
95+
.await
96+
.optional()?;
97+
98+
if let Some(deleted_crate) = deleted_crate {
99+
return Err(bad_request(format!(
100+
"A crate with the name `{}` was recently deleted. Reuse of this name will be available after {}.",
101+
deleted_crate.0,
102+
deleted_crate.1.to_rfc3339_opts(SecondsFormat::Secs, true)
103+
)));
104+
}
105+
89106
// this query should only be used for the endpoint scope calculation
90107
// since a race condition there would only cause `publish-new` instead of
91108
// `publish-update` to be used.
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use crate::models::NewDeletedCrate;
2+
use crate::tests::builders::PublishBuilder;
3+
use crate::tests::util::{RequestHelper, TestApp};
4+
use chrono::{Duration, Utc};
5+
use crates_io_database::schema::deleted_crates;
6+
use diesel_async::RunQueryDsl;
7+
use googletest::prelude::*;
8+
use http::StatusCode;
9+
use insta::assert_snapshot;
10+
11+
#[tokio::test(flavor = "multi_thread")]
12+
async fn test_recently_deleted_crate_with_same_name() -> anyhow::Result<()> {
13+
let (app, _, _, token) = TestApp::full().with_token();
14+
let mut conn = app.async_db_conn().await;
15+
16+
let now = Utc::now();
17+
let created_at = now - Duration::hours(24);
18+
let deleted_at = now - Duration::hours(1);
19+
let available_at = "2099-12-25T12:34:56Z".parse()?;
20+
21+
let deleted_crate = NewDeletedCrate::builder("actix_web")
22+
.created_at(&created_at)
23+
.deleted_at(&deleted_at)
24+
.available_at(&available_at)
25+
.build();
26+
27+
diesel::insert_into(deleted_crates::table)
28+
.values(deleted_crate)
29+
.execute(&mut conn)
30+
.await?;
31+
32+
let crate_to_publish = PublishBuilder::new("actix-web", "1.0.0");
33+
let response = token.publish_crate(crate_to_publish).await;
34+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
35+
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"A crate with the name `actix_web` was recently deleted. Reuse of this name will be available after 2099-12-25T12:34:56Z."}]}"#);
36+
assert_that!(app.stored_files().await, empty());
37+
38+
Ok(())
39+
}

src/tests/krate/publish/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod auth;
33
mod basics;
44
mod build_metadata;
55
mod categories;
6+
mod deleted_crates;
67
mod dependencies;
78
mod emails;
89
mod features;

0 commit comments

Comments
 (0)