diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap new file mode 100644 index 00000000000..af1afffa98b --- /dev/null +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap @@ -0,0 +1,62 @@ +--- +source: src/tests/krate/yanking.rs +expression: json +--- +{ + "version": { + "id": 1, + "crate": "patchable", + "num": "1.0.0", + "dl_path": "/api/v1/crates/patchable/1.0.0/download", + "readme_path": "/api/v1/crates/patchable/1.0.0/readme", + "updated_at": "[datetime]", + "created_at": "[datetime]", + "downloads": 0, + "features": {}, + "yanked": true, + "yank_message": "Yanking reason", + "lib_links": null, + "license": "MIT", + "links": { + "dependencies": "/api/v1/crates/patchable/1.0.0/dependencies", + "version_downloads": "/api/v1/crates/patchable/1.0.0/downloads", + "authors": "/api/v1/crates/patchable/1.0.0/authors" + }, + "crate_size": 151, + "published_by": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "audit_actions": [ + { + "action": "publish", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + } + ], + "checksum": "ddfc395ab340f413ee1d1ed0afce51a7c9df1c99c551fed5aef76edd4abe4048", + "rust_version": null, + "has_lib": false, + "bin_names": [] + } +} diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap new file mode 100644 index 00000000000..455b8364ca7 --- /dev/null +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap @@ -0,0 +1,73 @@ +--- +source: src/tests/krate/yanking.rs +expression: json +--- +{ + "version": { + "id": 1, + "crate": "patchable", + "num": "1.0.0", + "dl_path": "/api/v1/crates/patchable/1.0.0/download", + "readme_path": "/api/v1/crates/patchable/1.0.0/readme", + "updated_at": "[datetime]", + "created_at": "[datetime]", + "downloads": 0, + "features": {}, + "yanked": true, + "yank_message": "Updated reason", + "lib_links": null, + "license": "MIT", + "links": { + "dependencies": "/api/v1/crates/patchable/1.0.0/dependencies", + "version_downloads": "/api/v1/crates/patchable/1.0.0/downloads", + "authors": "/api/v1/crates/patchable/1.0.0/authors" + }, + "crate_size": 151, + "published_by": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "audit_actions": [ + { + "action": "publish", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + } + ], + "checksum": "ddfc395ab340f413ee1d1ed0afce51a7c9df1c99c551fed5aef76edd4abe4048", + "rust_version": null, + "has_lib": false, + "bin_names": [] + } +} diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap new file mode 100644 index 00000000000..455b8364ca7 --- /dev/null +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap @@ -0,0 +1,73 @@ +--- +source: src/tests/krate/yanking.rs +expression: json +--- +{ + "version": { + "id": 1, + "crate": "patchable", + "num": "1.0.0", + "dl_path": "/api/v1/crates/patchable/1.0.0/download", + "readme_path": "/api/v1/crates/patchable/1.0.0/readme", + "updated_at": "[datetime]", + "created_at": "[datetime]", + "downloads": 0, + "features": {}, + "yanked": true, + "yank_message": "Updated reason", + "lib_links": null, + "license": "MIT", + "links": { + "dependencies": "/api/v1/crates/patchable/1.0.0/dependencies", + "version_downloads": "/api/v1/crates/patchable/1.0.0/downloads", + "authors": "/api/v1/crates/patchable/1.0.0/authors" + }, + "crate_size": 151, + "published_by": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "audit_actions": [ + { + "action": "publish", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + } + ], + "checksum": "ddfc395ab340f413ee1d1ed0afce51a7c9df1c99c551fed5aef76edd4abe4048", + "rust_version": null, + "has_lib": false, + "bin_names": [] + } +} diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap new file mode 100644 index 00000000000..e441d6b90d1 --- /dev/null +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap @@ -0,0 +1,84 @@ +--- +source: src/tests/krate/yanking.rs +expression: json +--- +{ + "version": { + "id": 1, + "crate": "patchable", + "num": "1.0.0", + "dl_path": "/api/v1/crates/patchable/1.0.0/download", + "readme_path": "/api/v1/crates/patchable/1.0.0/readme", + "updated_at": "[datetime]", + "created_at": "[datetime]", + "downloads": 0, + "features": {}, + "yanked": false, + "yank_message": null, + "lib_links": null, + "license": "MIT", + "links": { + "dependencies": "/api/v1/crates/patchable/1.0.0/dependencies", + "version_downloads": "/api/v1/crates/patchable/1.0.0/downloads", + "authors": "/api/v1/crates/patchable/1.0.0/authors" + }, + "crate_size": 151, + "published_by": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "audit_actions": [ + { + "action": "publish", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "unyank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + } + ], + "checksum": "ddfc395ab340f413ee1d1ed0afce51a7c9df1c99c551fed5aef76edd4abe4048", + "rust_version": null, + "has_lib": false, + "bin_names": [] + } +} diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap new file mode 100644 index 00000000000..e441d6b90d1 --- /dev/null +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap @@ -0,0 +1,84 @@ +--- +source: src/tests/krate/yanking.rs +expression: json +--- +{ + "version": { + "id": 1, + "crate": "patchable", + "num": "1.0.0", + "dl_path": "/api/v1/crates/patchable/1.0.0/download", + "readme_path": "/api/v1/crates/patchable/1.0.0/readme", + "updated_at": "[datetime]", + "created_at": "[datetime]", + "downloads": 0, + "features": {}, + "yanked": false, + "yank_message": null, + "lib_links": null, + "license": "MIT", + "links": { + "dependencies": "/api/v1/crates/patchable/1.0.0/dependencies", + "version_downloads": "/api/v1/crates/patchable/1.0.0/downloads", + "authors": "/api/v1/crates/patchable/1.0.0/authors" + }, + "crate_size": 151, + "published_by": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "audit_actions": [ + { + "action": "publish", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "unyank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + } + ], + "checksum": "ddfc395ab340f413ee1d1ed0afce51a7c9df1c99c551fed5aef76edd4abe4048", + "rust_version": null, + "has_lib": false, + "bin_names": [] + } +} diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap new file mode 100644 index 00000000000..af1afffa98b --- /dev/null +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap @@ -0,0 +1,62 @@ +--- +source: src/tests/krate/yanking.rs +expression: json +--- +{ + "version": { + "id": 1, + "crate": "patchable", + "num": "1.0.0", + "dl_path": "/api/v1/crates/patchable/1.0.0/download", + "readme_path": "/api/v1/crates/patchable/1.0.0/readme", + "updated_at": "[datetime]", + "created_at": "[datetime]", + "downloads": 0, + "features": {}, + "yanked": true, + "yank_message": "Yanking reason", + "lib_links": null, + "license": "MIT", + "links": { + "dependencies": "/api/v1/crates/patchable/1.0.0/dependencies", + "version_downloads": "/api/v1/crates/patchable/1.0.0/downloads", + "authors": "/api/v1/crates/patchable/1.0.0/authors" + }, + "crate_size": 151, + "published_by": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "audit_actions": [ + { + "action": "publish", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + }, + { + "action": "yank", + "user": { + "id": 1, + "login": "foo", + "name": null, + "avatar": null, + "url": "https://github.com/foo" + }, + "time": "[datetime]" + } + ], + "checksum": "ddfc395ab340f413ee1d1ed0afce51a7c9df1c99c551fed5aef76edd4abe4048", + "rust_version": null, + "has_lib": false, + "bin_names": [] + } +} diff --git a/src/tests/krate/yanking.rs b/src/tests/krate/yanking.rs index c8216355cda..1dc3aa83695 100644 --- a/src/tests/krate/yanking.rs +++ b/src/tests/krate/yanking.rs @@ -3,9 +3,11 @@ use crate::schema::publish_limit_buckets; use crate::tests::builders::PublishBuilder; use crate::tests::routes::crates::versions::yank_unyank::YankRequestHelper; use crate::tests::util::{RequestHelper, TestApp}; +use crate::tests::VersionResponse; use chrono::Utc; use diesel::{ExpressionMethods, RunQueryDsl}; use googletest::prelude::*; +use insta::assert_json_snapshot; use std::time::Duration; #[tokio::test(flavor = "multi_thread")] @@ -220,3 +222,68 @@ async fn publish_after_yank_max_version() { let json = anon.show_crate("fyk_max").await; assert_eq!(json.krate.max_version, "2.0.0"); } + +#[tokio::test(flavor = "multi_thread")] +async fn patch_version_yank_unyank() { + let (_, anon, _, token) = TestApp::full().with_token(); + + // Upload a new crate + let crate_to_publish = PublishBuilder::new("patchable", "1.0.0"); + token.publish_crate(crate_to_publish).await.good(); + + // Check initial state + let json = anon.show_version("patchable", "1.0.0").await; + assert!(!json.version.yanked); + assert_eq!(json.version.yank_message, None); + + let assert_json_helper = |json: VersionResponse| { + assert_json_snapshot!(json, { + ".version.created_at" => "[datetime]", + ".version.updated_at" => "[datetime]", + ".version.audit_actions[].time" => "[datetime]", + }); + }; + + // Yank with message + let response = token + .update_yank_status("patchable", "1.0.0", Some(true), Some("Yanking reason")) + .await + .good(); + assert_json_helper(response); + + let json = anon.show_version("patchable", "1.0.0").await; + assert_json_helper(json); + + // Update yank message + let response = token + .update_yank_status("patchable", "1.0.0", None, Some("Updated reason")) + .await + .good(); + assert_json_helper(response); + + let json = anon.show_version("patchable", "1.0.0").await; + assert_json_helper(json); + + // Unyank + let response = token + .update_yank_status("patchable", "1.0.0", Some(false), None) + .await + .good(); + assert_json_helper(response); + + let json = anon.show_version("patchable", "1.0.0").await; + assert_json_helper(json); + + // Attempt to set yank message on unyanked version (should fail) + token + .update_yank_status("patchable", "1.0.0", None, Some("Invalid message")) + .await + .status() + .is_client_error(); + // Attempt to unyank with message (should fail) + token + .update_yank_status("patchable", "1.0.0", Some(false), Some("Invalid message")) + .await + .status() + .is_client_error(); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 7b7abe2c8ac..1457059fad5 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -56,7 +56,7 @@ pub struct CrateResponse { versions: Option>, keywords: Option>, } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct VersionResponse { version: EncodableVersion, } diff --git a/src/tests/routes/crates/versions/yank_unyank.rs b/src/tests/routes/crates/versions/yank_unyank.rs index f802ddb6528..26011389291 100644 --- a/src/tests/routes/crates/versions/yank_unyank.rs +++ b/src/tests/routes/crates/versions/yank_unyank.rs @@ -1,8 +1,9 @@ use crate::tests::builders::{CrateBuilder, PublishBuilder}; use crate::tests::util::{RequestHelper, Response, TestApp}; -use crate::tests::OkBool; +use crate::tests::{OkBool, VersionResponse}; use http::StatusCode; use insta::assert_snapshot; +use serde_json::json; pub trait YankRequestHelper { /// Yank the specified version of the specified crate and run all pending background jobs @@ -10,6 +11,15 @@ pub trait YankRequestHelper { /// Unyank the specified version of the specified crate and run all pending background jobs async fn unyank(&self, krate_name: &str, version: &str) -> Response; + + /// Update the yank status of the specified version of the specified crate with a patch request and run all pending background jobs + async fn update_yank_status( + &self, + krate_name: &str, + version: &str, + yanked: Option, + yank_message: Option<&str>, + ) -> Response; } impl YankRequestHelper for T { @@ -26,6 +36,28 @@ impl YankRequestHelper for T { self.app().run_pending_background_jobs().await; response } + + async fn update_yank_status( + &self, + krate_name: &str, + version: &str, + yanked: Option, + yank_message: Option<&str>, + ) -> Response { + let url = format!("/api/v1/crates/{krate_name}/{version}"); + + let json_body = json!({ + "version": { + "yanked": yanked, + "yank_message": yank_message + } + }); + let body = serde_json::to_string(&json_body).expect("Failed to serialize JSON body"); + + let response = self.patch(&url, body).await; + self.app().run_pending_background_jobs().await; + response + } } #[tokio::test(flavor = "multi_thread")] diff --git a/src/tests/util.rs b/src/tests/util.rs index b6b58432d84..aa096506dd5 100644 --- a/src/tests/util.rs +++ b/src/tests/util.rs @@ -145,6 +145,20 @@ pub trait RequestHelper { self.run(request).await } + /// Issue a PATCH request + async fn patch(&self, path: &str, body: impl Into) -> Response { + let body = body.into(); + let is_json = body.starts_with(b"{") && body.ends_with(b"}"); + + let mut request = self.request_builder(Method::PATCH, path); + *request.body_mut() = body; + if is_json { + request.header(header::CONTENT_TYPE, "application/json"); + } + + self.run(request).await + } + /// Issue a DELETE request async fn delete(&self, path: &str) -> Response { let request = self.request_builder(Method::DELETE, path);